Kevin Smith avatar Kevin Smith committed 89c0620

Integrating erlang_js into riak releases

Comments (0)

Files changed (29)

apps/erlang_js/Makefile

+all:
+	./rebar compile
+
+clean:
+	./rebar clean
+
+test: all
+	@cd tests;erl -make
+	@erl -noshell -boot start_sasl -pa ebin -s erlang_js -eval 'test_suite:test().' -s init stop
+	@rm -f ebin/test_* ebin/*_tests.erl

apps/erlang_js/c_src/Makefile

+# This Makefile builds the dependencies (libjs and libnspr) needed by
+# spidermonkey_drv.so
+
+js: libjs.a
+
+libnspr4.a: deps/nspr_release/dist/lib/libnspr4.a
+	@cp deps/nspr_release/dist/lib/libnspr4.a .
+
+libjs.a: libnspr4.a js/src/libjs.a
+	@cp deps/js/src/libjs.a .
+
+deps/nspr_release/dist/lib/libnspr4.a: deps/mozilla deps/nspr_release
+	@cd deps/nspr_release;../mozilla/nsprpub/configure --disable-debug --enable-optimize;make
+
+deps/mozilla:
+	@mkdir -p $(@)
+	@cvs -q -d :pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot co -r NSPR_4_8_RTM mozilla/nsprpub
+	@mv mozilla deps
+
+deps/nspr_release:
+	@mkdir -p $(@)
+
+js/src/libjs.a: deps/js
+	@cd deps/js/src;make BUILD_OPT=1 JS_DIST=../../nspr_release/dist JS_THREADSAFE=1 XCFLAGS="-DHAVE_VA_COPY -DVA_COPY=va_copy" -f Makefile.ref
+	@cp deps/js/src/*_OPT.OBJ/libjs.a deps/js/src
+	@mkdir -p include/js;
+	@cp deps/js/src/*.h include/js
+	@cp deps/js/src/*.tbl include/js
+	@cp deps/js/src/*_OPT.OBJ/*.h include/js
+
+deps/js:
+	@tar -C deps -xzf js-1.8.0-rc1.tar.gz
+
+dist:
+	@rm -f libjs.a libnspr4.a
+	@rm -f *flymake*
+	@rm -rf include
+	@rm -rf deps
+
+jsclean:
+	@rm -rf deps
+	@rm -f *flymake*
+	@rm -f libjs.a libnspr4.a
+	@rm -rf include

apps/erlang_js/c_src/build_driver_deps.sh

+#!/bin/bash
+
+set -e
+
+if [ `basename $PWD` != "src" ]; then
+    pushd c_src
+fi
+
+unset CFLAGS LDFLAGS
+
+make $1

apps/erlang_js/c_src/config.h

+/*
+Copyright (c) 2009 Hypothetical Labs, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/

apps/erlang_js/c_src/driver_comm.c

+/*
+Copyright (c) 2009 Hypothetical Labs, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+#define COMMAND_SIZE 2
+
+#include <string.h>
+#include <strings.h>
+
+#include <erl_driver.h>
+
+#include "driver_comm.h"
+
+inline int read_int32(char **data) {
+  char *d = *data;
+  int value = ((((int)(((unsigned char*) (d))[0]))  << 24) |
+	       (((int)(((unsigned char*) (d))[1]))  << 16) |
+	       (((int)(((unsigned char*) (d))[2]))  << 8)  |
+	       (((int)(((unsigned char*) (d))[3]))));
+  (*data) += 4;
+  return value;
+}
+
+char *read_command(char **data) {
+  char *buf = (char *) driver_alloc(COMMAND_SIZE + 1);
+  memset(buf, 0, COMMAND_SIZE + 1);
+  memcpy(buf, (const char *) *data, COMMAND_SIZE);
+  (*data) += 2;
+  return buf;
+}
+
+char *read_string(char **data) {
+  int length = read_int32(data);
+  char *buf = NULL;
+  if (length > 0) {
+    buf = (char *) driver_alloc(length + 1);
+    memset(buf, 0, length + 1);
+    memcpy(buf, (const char *) *data, length);
+    (*data) += length;
+  }
+  return buf;
+}

apps/erlang_js/c_src/driver_comm.h

+#ifndef __DRIVER_COMM__
+#define __DRIVER_COMM__
+
+int read_int32(char **data);
+
+/* Command strings will be two characters long */
+/* and must be freed via driver_free()         */
+char *read_command(char **data);
+
+/* Any string read with this function must be */
+/* freed with driver_free()                   */
+char *read_string(char **data);
+
+#endif
Add a comment to this file

apps/erlang_js/c_src/js-1.8.0-rc1.tar.gz

Binary file added.

apps/erlang_js/c_src/spidermonkey.c

+#include <string.h>
+#include <erl_driver.h>
+
+#include "spidermonkey.h"
+
+/* The class of the global object. */
+static JSClass global_class = {
+    "global", JSCLASS_GLOBAL_FLAGS,
+    JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub,
+    JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub,
+    JSCLASS_NO_OPTIONAL_MEMBERS
+};
+
+char *copy_string(const char *source) {
+  int size = strlen(source) + 1;
+  char *retval = driver_alloc(size);
+  memset(retval, 0, size);
+  strncpy(retval, source, size - 1);
+  return retval;
+}
+
+char *copy_jsstring(JSString *source) {
+  char *buf = JS_GetStringBytes(source);
+  return copy_string(buf);
+}
+
+inline void begin_request(spidermonkey_vm *vm) {
+  JS_SetContextThread(vm->context);
+  JS_BeginRequest(vm->context);
+}
+
+inline void end_request(spidermonkey_vm *vm) {
+  JS_EndRequest(vm->context);
+  JS_ClearContextThread(vm->context);
+}
+
+void on_error(JSContext *context, const char *message, JSErrorReport *report) {
+  if (report->flags & JSREPORT_EXCEPTION) {
+    spidermonkey_error *sm_error = (spidermonkey_error *) driver_alloc(sizeof(spidermonkey_error));
+    sm_error->msg = copy_string(message);
+    sm_error->lineno = report->lineno;
+    sm_error->offending_source = copy_string(report->linebuf);
+    JS_SetContextPrivate(context, sm_error);
+  }
+}
+
+spidermonkey_vm *sm_initialize() {
+  spidermonkey_vm *vm = (spidermonkey_vm*) driver_alloc(sizeof(spidermonkey_vm));
+  vm->runtime = JS_NewRuntime(MAX_GC_SIZE);
+  JS_SetGCParameter(vm->runtime, JSGC_MAX_BYTES, CONTEXT_HEAP_SIZE);
+  JS_SetGCParameter(vm->runtime, JSGC_MAX_MALLOC_BYTES, LAST_DITCH_GC_THRESHOLD);
+  vm->context = JS_NewContext(vm->runtime, CONTEXT_THREAD_STACK_SIZE);
+  begin_request(vm);
+  JS_SetOptions(vm->context, JSOPTION_VAROBJFIX);
+  JS_SetOptions(vm->context, JSOPTION_STRICT);
+  JS_SetOptions(vm->context, JSOPTION_COMPILE_N_GO);
+  JS_SetOptions(vm->context, JSVERSION_LATEST);
+  vm->global = JS_NewObject(vm->context, &global_class, NULL, NULL);
+  JS_InitStandardClasses(vm->context, vm->global);
+  JS_SetErrorReporter(vm->context, on_error);
+  end_request(vm);
+  return vm;
+}
+
+void sm_stop(spidermonkey_vm *vm) {
+  JS_SetContextThread(vm->context);
+  JS_DestroyContext(vm->context);
+  JS_DestroyRuntime(vm->runtime);
+  driver_free(vm);
+}
+
+void sm_shutdown() {
+  JS_ShutDown();
+}
+
+char *error_to_json(const spidermonkey_error *error) {
+  /* Allocate 1K to build error (hopefully that's enough!) */
+  int size = sizeof(char) * 1024;
+  char *retval = (char *) driver_alloc(size);
+  snprintf(retval, size, "{\"error\": {\"lineno\": %d, \"message\": \"%s\", \"source\": \"%s\"}}", error->lineno,
+	   error->msg, error->offending_source);
+  return retval;
+}
+
+void free_error(spidermonkey_error *error) {
+  driver_free(error->offending_source);
+  driver_free(error->msg);
+  driver_free(error);
+}
+
+char *sm_eval(spidermonkey_vm *vm, const char *filename, const char *code, int handle_retval) {
+  char *retval = NULL;
+  JSScript *script;
+  jsval result;
+
+  begin_request(vm);
+  script = JS_CompileScript(vm->context,
+			    vm->global,
+			    code, strlen(code),
+			    filename, 1);
+  spidermonkey_error *error = (spidermonkey_error *) JS_GetContextPrivate(vm->context);
+  if (error == NULL) {
+    JS_ClearPendingException(vm->context);
+    JS_ExecuteScript(vm->context, vm->global, script, &result);
+    if (handle_retval) {
+      if (JSVAL_IS_STRING(result)) {
+	JSString *str = JS_ValueToString(vm->context, result);
+	retval = copy_jsstring(str);
+      }
+      else {
+	retval = copy_string("{error: non_json_return}");
+      }
+    }
+    JS_DestroyScript(vm->context, script);
+  }
+  else {
+    retval = error_to_json(error);
+    free_error(error);
+    JS_SetContextPrivate(vm->context, NULL);
+  }
+  JS_MaybeGC(vm->context);
+  end_request(vm);
+  return retval;
+}

apps/erlang_js/c_src/spidermonkey.h

+#ifndef __SPIDERMONKEY_INTERFACE_
+#define __SPIDERMONKEY_INTERFACE_
+
+#include "jsapi.h"
+
+typedef struct _spidermonkey_error_t {
+  unsigned int lineno;
+  char *msg;
+  char *offending_source;
+} spidermonkey_error;
+
+typedef struct _spidermonkey_vm_t {
+  JSRuntime* runtime;
+  JSContext* context;
+  JSObject* global;
+} spidermonkey_vm;
+
+/* Bytes to allocate before GC */
+#define MAX_GC_SIZE 1024 * 1024
+
+/* 8K stack size for each context */
+#define CONTEXT_THREAD_STACK_SIZE 8192
+
+/* 8MB heap for each context */
+#define CONTEXT_HEAP_SIZE 8 * 1024 * 1024
+
+/* 8MB last ditch GC threshold */
+#define LAST_DITCH_GC_THRESHOLD CONTEXT_HEAP_SIZE
+
+spidermonkey_vm *sm_initialize();
+
+void sm_stop(spidermonkey_vm *vm);
+
+void sm_shutdown();
+
+char *sm_eval(spidermonkey_vm *vm, const char *filename, const char *code, int handle_retval);
+
+#endif

apps/erlang_js/c_src/spidermonkey_drv.c

+#include <string.h>
+#include <erl_driver.h>
+/* #include <ei.h> */
+
+#include "spidermonkey.h"
+#include "config.h"
+#include "driver_comm.h"
+
+typedef struct _spidermonkey_drv_t {
+  ErlDrvPort port;
+  spidermonkey_vm *vm;
+  ErlDrvTermData atom_ok;
+  ErlDrvTermData atom_error;
+  ErlDrvTermData atom_unknown_cmd;
+  int shutdown;
+} spidermonkey_drv_t;
+
+
+/* Forward declarations */
+static ErlDrvData start(ErlDrvPort port, char *cmd);
+static void stop(ErlDrvData handle);
+static void process(ErlDrvData handle, ErlIOVec *ev);
+
+static ErlDrvEntry spidermonkey_drv_entry = {
+    NULL,                             /* init */
+    start,                            /* startup */
+    stop,                             /* shutdown */
+    NULL,                             /* output */
+    NULL,                             /* ready_input */
+    NULL,                             /* ready_output */
+    (char *) "spidermonkey_drv",      /* the name of the driver */
+    NULL,                             /* finish */
+    NULL,                             /* handle */
+    NULL,                             /* control */
+    NULL,                             /* timeout */
+    process,                          /* process */
+    NULL,                             /* ready_async */
+    NULL,                             /* flush */
+    NULL,                             /* call */
+    NULL,                             /* event */
+    ERL_DRV_EXTENDED_MARKER,          /* ERL_DRV_EXTENDED_MARKER */
+    ERL_DRV_EXTENDED_MAJOR_VERSION,   /* ERL_DRV_EXTENDED_MAJOR_VERSION */
+    ERL_DRV_EXTENDED_MAJOR_VERSION,   /* ERL_DRV_EXTENDED_MINOR_VERSION */
+    ERL_DRV_FLAG_USE_PORT_LOCKING     /* ERL_DRV_FLAGs */
+};
+
+
+static void send_output(ErlDrvPort port, ErlDrvTermData *terms, int term_count) {
+  driver_output_term(port, terms, term_count);
+}
+
+static void send_ok_response(spidermonkey_drv_t *dd) {
+  ErlDrvTermData terms[] = {ERL_DRV_ATOM, dd->atom_ok};
+  send_output(dd->port, terms, sizeof(terms) / sizeof(terms[0]));
+}
+
+static void send_error_string_response(spidermonkey_drv_t *dd, const char *msg) {
+  ErlDrvTermData terms[] = {ERL_DRV_ATOM, dd->atom_error,
+			    ERL_DRV_BUF2BINARY, (ErlDrvTermData) msg, strlen(msg),
+			    ERL_DRV_TUPLE, 2};
+  send_output(dd->port, terms, sizeof(terms) / sizeof(terms[0]));
+}
+
+static void send_string_response(spidermonkey_drv_t *dd, const char *result) {
+  ErlDrvTermData terms[] = {ERL_DRV_ATOM, dd->atom_ok,
+			    ERL_DRV_BUF2BINARY, (ErlDrvTermData) result, strlen(result),
+			    ERL_DRV_TUPLE, 2};
+  send_output(dd->port, terms, sizeof(terms) / sizeof(terms[0]));
+}
+
+static void unknown_command(spidermonkey_drv_t *dd) {
+  ErlDrvTermData terms[] = {ERL_DRV_ATOM, dd->atom_error,
+			    ERL_DRV_ATOM, dd->atom_unknown_cmd,
+			    ERL_DRV_TUPLE, 2};
+  send_output(dd->port, terms, sizeof(terms) / sizeof(terms[0]));
+}
+
+DRIVER_INIT(spidermonkey_drv) {
+  return &spidermonkey_drv_entry;
+}
+
+static ErlDrvData start(ErlDrvPort port, char *cmd) {
+  spidermonkey_drv_t *retval = (spidermonkey_drv_t*) driver_alloc(sizeof(spidermonkey_drv_t));
+  retval->port = port;
+  retval->shutdown = 0;
+  retval->atom_ok = driver_mk_atom((char *) "ok");
+  retval->atom_error = driver_mk_atom((char *) "error");
+  retval->atom_unknown_cmd = driver_mk_atom((char *) "unknown_command");
+  retval->vm = sm_initialize();
+  return (ErlDrvData) retval;
+}
+
+static void stop(ErlDrvData handle) {
+  spidermonkey_drv_t *dd = (spidermonkey_drv_t*) handle;
+  sm_stop(dd->vm);
+  if (dd->shutdown) {
+    sm_shutdown();
+  }
+  driver_free(dd);
+}
+
+static void process(ErlDrvData handle, ErlIOVec *ev) {
+  spidermonkey_drv_t *dd = (spidermonkey_drv_t*) handle;
+  ErlDrvBinary *args = ev->binv[1];
+  char *data = args->orig_bytes;
+  char *command = read_command(&data);
+  char *result = NULL;
+  if (strncmp(command, "ej", 2) == 0) {
+    char *filename = read_string(&data);
+    char *code = read_string(&data);
+    result = sm_eval(dd->vm, filename, code, 1);
+    if (strstr(result, "{\"error\"") != NULL) {
+      send_error_string_response(dd, result);
+    }
+    else {
+      send_string_response(dd, result);
+    }
+    driver_free(filename);
+    driver_free(code);
+    driver_free(result);
+  }
+  else if (strncmp(command, "dj", 2) == 0) {
+    char *filename = read_string(&data);
+    char *code = read_string(&data);
+    result = sm_eval(dd->vm, filename, code, 0);
+    if (result == NULL) {
+      send_ok_response(dd);
+    }
+    else {
+      send_error_string_response(dd, result);
+      driver_free(result);
+    }
+    driver_free(filename);
+    driver_free(code);
+  }
+  else if (strncmp(command, "sd", 2) == 0) {
+    dd->shutdown = 1;
+    send_ok_response(dd);
+  }
+  else {
+    unknown_command(dd);
+  }
+  driver_free(command);
+}

apps/erlang_js/priv/json2.js

+/*
+    http://www.JSON.org/json2.js
+    2009-08-17
+
+    Public Domain.
+
+    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+    See http://www.JSON.org/js.html
+
+    This file creates a global JSON object containing two methods: stringify
+    and parse.
+
+        JSON.stringify(value, replacer, space)
+            value       any JavaScript value, usually an object or array.
+
+            replacer    an optional parameter that determines how object
+                        values are stringified for objects. It can be a
+                        function or an array of strings.
+
+            space       an optional parameter that specifies the indentation
+                        of nested structures. If it is omitted, the text will
+                        be packed without extra whitespace. If it is a number,
+                        it will specify the number of spaces to indent at each
+                        level. If it is a string (such as '\t' or '&nbsp;'),
+                        it contains the characters used to indent at each level.
+
+            This method produces a JSON text from a JavaScript value.
+
+            When an object value is found, if the object contains a toJSON
+            method, its toJSON method will be called and the result will be
+            stringified. A toJSON method does not serialize: it returns the
+            value represented by the name/value pair that should be serialized,
+            or undefined if nothing should be serialized. The toJSON method
+            will be passed the key associated with the value, and this will be
+            bound to the value
+
+            For example, this would serialize Dates as ISO strings.
+
+                Date.prototype.toJSON = function (key) {
+                    function f(n) {
+                        // Format integers to have at least two digits.
+                        return n < 10 ? '0' + n : n;
+                    }
+
+                    return this.getUTCFullYear()   + '-' +
+                         f(this.getUTCMonth() + 1) + '-' +
+                         f(this.getUTCDate())      + 'T' +
+                         f(this.getUTCHours())     + ':' +
+                         f(this.getUTCMinutes())   + ':' +
+                         f(this.getUTCSeconds())   + 'Z';
+                };
+
+            You can provide an optional replacer method. It will be passed the
+            key and value of each member, with this bound to the containing
+            object. The value that is returned from your method will be
+            serialized. If your method returns undefined, then the member will
+            be excluded from the serialization.
+
+            If the replacer parameter is an array of strings, then it will be
+            used to select the members to be serialized. It filters the results
+            such that only members with keys listed in the replacer array are
+            stringified.
+
+            Values that do not have JSON representations, such as undefined or
+            functions, will not be serialized. Such values in objects will be
+            dropped; in arrays they will be replaced with null. You can use
+            a replacer function to replace those with JSON values.
+            JSON.stringify(undefined) returns undefined.
+
+            The optional space parameter produces a stringification of the
+            value that is filled with line breaks and indentation to make it
+            easier to read.
+
+            If the space parameter is a non-empty string, then that string will
+            be used for indentation. If the space parameter is a number, then
+            the indentation will be that many spaces.
+
+            Example:
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}]);
+            // text is '["e",{"pluribus":"unum"}]'
+
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+            text = JSON.stringify([new Date()], function (key, value) {
+                return this[key] instanceof Date ?
+                    'Date(' + this[key] + ')' : value;
+            });
+            // text is '["Date(---current time---)"]'
+
+
+        JSON.parse(text, reviver)
+            This method parses a JSON text to produce an object or array.
+            It can throw a SyntaxError exception.
+
+            The optional reviver parameter is a function that can filter and
+            transform the results. It receives each of the keys and values,
+            and its return value is used instead of the original value.
+            If it returns what it received, then the structure is not modified.
+            If it returns undefined then the member is deleted.
+
+            Example:
+
+            // Parse the text. Values that look like ISO date strings will
+            // be converted to Date objects.
+
+            myData = JSON.parse(text, function (key, value) {
+                var a;
+                if (typeof value === 'string') {
+                    a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+                    if (a) {
+                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+                            +a[5], +a[6]));
+                    }
+                }
+                return value;
+            });
+
+            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+                var d;
+                if (typeof value === 'string' &&
+                        value.slice(0, 5) === 'Date(' &&
+                        value.slice(-1) === ')') {
+                    d = new Date(value.slice(5, -1));
+                    if (d) {
+                        return d;
+                    }
+                }
+                return value;
+            });
+
+
+    This is a reference implementation. You are free to copy, modify, or
+    redistribute.
+
+    This code should be minified before deployment.
+    See http://javascript.crockford.com/jsmin.html
+
+    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+    NOT CONTROL.
+*/
+
+/*jslint evil: true */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+    lastIndex, length, parse, prototype, push, replace, slice, stringify,
+    test, toJSON, toString, valueOf
+*/
+
+"use strict";
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+    this.JSON = {};
+}
+
+(function () {
+
+    function f(n) {
+        // Format integers to have at least two digits.
+        return n < 10 ? '0' + n : n;
+    }
+
+    if (typeof Date.prototype.toJSON !== 'function') {
+
+        Date.prototype.toJSON = function (key) {
+
+            return isFinite(this.valueOf()) ?
+                   this.getUTCFullYear()   + '-' +
+                 f(this.getUTCMonth() + 1) + '-' +
+                 f(this.getUTCDate())      + 'T' +
+                 f(this.getUTCHours())     + ':' +
+                 f(this.getUTCMinutes())   + ':' +
+                 f(this.getUTCSeconds())   + 'Z' : null;
+        };
+
+        String.prototype.toJSON =
+        Number.prototype.toJSON =
+        Boolean.prototype.toJSON = function (key) {
+            return this.valueOf();
+        };
+    }
+
+    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        gap,
+        indent,
+        meta = {    // table of character substitutions
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '"' : '\\"',
+            '\\': '\\\\'
+        },
+        rep;
+
+
+    function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+        escapable.lastIndex = 0;
+        return escapable.test(string) ?
+            '"' + string.replace(escapable, function (a) {
+                var c = meta[a];
+                return typeof c === 'string' ? c :
+                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+            }) + '"' :
+            '"' + string + '"';
+    }
+
+
+    function str(key, holder) {
+
+// Produce a string from holder[key].
+
+        var i,          // The loop counter.
+            k,          // The member key.
+            v,          // The member value.
+            length,
+            mind = gap,
+            partial,
+            value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+        if (value && typeof value === 'object' &&
+                typeof value.toJSON === 'function') {
+            value = value.toJSON(key);
+        }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+        if (typeof rep === 'function') {
+            value = rep.call(holder, key, value);
+        }
+
+// What happens next depends on the value's type.
+
+        switch (typeof value) {
+        case 'string':
+            return quote(value);
+
+        case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+            return isFinite(value) ? String(value) : 'null';
+
+        case 'boolean':
+        case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+            return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+        case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+            if (!value) {
+                return 'null';
+            }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+            gap += indent;
+            partial = [];
+
+// Is the value an array?
+
+            if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+                length = value.length;
+                for (i = 0; i < length; i += 1) {
+                    partial[i] = str(i, value) || 'null';
+                }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+                v = partial.length === 0 ? '[]' :
+                    gap ? '[\n' + gap +
+                            partial.join(',\n' + gap) + '\n' +
+                                mind + ']' :
+                          '[' + partial.join(',') + ']';
+                gap = mind;
+                return v;
+            }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+            if (rep && typeof rep === 'object') {
+                length = rep.length;
+                for (i = 0; i < length; i += 1) {
+                    k = rep[i];
+                    if (typeof k === 'string') {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+                for (k in value) {
+                    if (Object.hasOwnProperty.call(value, k)) {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+            v = partial.length === 0 ? '{}' :
+                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+                        mind + '}' : '{' + partial.join(',') + '}';
+            gap = mind;
+            return v;
+        }
+    }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+    if (typeof JSON.stringify !== 'function') {
+        JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+            var i;
+            gap = '';
+            indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+            if (typeof space === 'number') {
+                for (i = 0; i < space; i += 1) {
+                    indent += ' ';
+                }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+            } else if (typeof space === 'string') {
+                indent = space;
+            }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+            rep = replacer;
+            if (replacer && typeof replacer !== 'function' &&
+                    (typeof replacer !== 'object' ||
+                     typeof replacer.length !== 'number')) {
+                throw new Error('JSON.stringify');
+            }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+            return str('', {'': value});
+        };
+    }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+    if (typeof JSON.parse !== 'function') {
+        JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+            var j;
+
+            function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+                var k, v, value = holder[key];
+                if (value && typeof value === 'object') {
+                    for (k in value) {
+                        if (Object.hasOwnProperty.call(value, k)) {
+                            v = walk(value, k);
+                            if (v !== undefined) {
+                                value[k] = v;
+                            } else {
+                                delete value[k];
+                            }
+                        }
+                    }
+                }
+                return reviver.call(holder, key, value);
+            }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+            cx.lastIndex = 0;
+            if (cx.test(text)) {
+                text = text.replace(cx, function (a) {
+                    return '\\u' +
+                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+                });
+            }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+            if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+                j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+                return typeof reviver === 'function' ?
+                    walk({'': j}, '') : j;
+            }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+            throw new SyntaxError('JSON.parse');
+        };
+    }
+}());
Add a comment to this file

apps/erlang_js/priv/spidermonkey_drv.so

Binary file added.

apps/erlang_js/rebar.config

+{cover_enabled, true}.
+{erl_opts, [debug_info, fail_on_warning]}.
+{lib_dirs, [".."]}.
+
+%% Linked-in driver config
+{so_name, "spidermonkey"}.
+{port_envs, [{"CFLAGS",  "$CFLAGS -I c_src/include/js -DXP_UNIX"},
+             {"LDFLAGS", "$LDFLAGS c_src/libjs.a c_src/libnspr4.a"}]}.
+{port_pre_script, {"c_src/build_driver_deps.sh", "js"}}.
+{port_cleanup_script, "c_src/build_driver_deps.sh jsclean"}.

apps/erlang_js/src/Emakefile

+{"*", [warn_obsolete_guard, warn_unused_import,
+       warn_shadow_vars, warn_export_vars, debug_info,
+       {i, "../include"},
+       {outdir, "../ebin"}]}.

apps/erlang_js/src/erlang_js.erl

+-module(erlang_js).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/0, start/2, stop/1]).
+
+start() ->
+  start_deps([sasl]),
+  application:start(erlang_js).
+
+start(_StartType, _StartArgs) ->
+  erlang_js_sup:start_link().
+
+stop(_State) ->
+  ok.
+
+%% Internal functions
+start_deps([]) ->
+  ok;
+start_deps([App|T]) ->
+  case is_running(App, application:which_applications()) of
+    false ->
+      ok = application:start(App);
+    true ->
+      ok
+  end,
+  start_deps(T).
+
+is_running(_App, []) ->
+  false;
+is_running(App, [{App, _, _}|_]) ->
+  true;
+is_running(App, [_|T]) ->
+  is_running(App, T).

apps/erlang_js/src/erlang_js_sup.erl

+-module(erlang_js_sup).
+
+-behaviour(supervisor).
+
+%% API
+-export([start_link/0]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+-define(SERVER, ?MODULE).
+
+start_link() ->
+  supervisor:start_link({local, ?SERVER}, ?MODULE, []).
+
+init([]) ->
+  RestartStrategy = one_for_one,
+  MaxRestarts = 1000,
+  MaxSecondsBetweenRestarts = 3600,
+
+  SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},
+
+  Restart = permanent,
+  Shutdown = 2000,
+  Type = worker,
+
+  case js_driver:load_driver() of
+    false ->
+      throw({error, {load_error, "Failed to load spidermonkey_drv.so"}});
+    true ->
+      Cache = {cache, {js_cache, start_link, []},
+               Restart, Shutdown, Type, [js_cache]},
+
+      {ok, {SupFlags, [Cache]}}
+  end.

apps/erlang_js/src/js.erl

+-module(js).
+
+-export([define/2, define/3, eval/2, call/3, call/4]).
+
+define(Ctx, Js) ->
+  define(Ctx, Js, []).
+
+define(Ctx, Js, Bindings) ->
+  JsBindings = list_to_binary(build_bindings(Bindings, [])),
+  FinalJs = iolist_to_binary([JsBindings, Js]),
+  js_driver:define_js(Ctx, FinalJs).
+
+eval(Ctx, Js) ->
+  js_driver:eval_js(Ctx, Js).
+
+call(Ctx, FunctionName, Args) ->
+  call(Ctx, FunctionName, Args, []).
+
+call(Ctx, FunctionName, Args, Bindings) ->
+  JsBindings = list_to_binary(build_bindings(Bindings, [])),
+  ArgList = build_arg_list(Args, []),
+  Js = iolist_to_binary([JsBindings, FunctionName, "(", ArgList, ");"]),
+  js_driver:eval_js(Ctx, Js).
+
+%% Internal functions
+build_bindings([], Accum) ->
+  Accum;
+build_bindings([{VarName, Value}|T], Accum) ->
+  FinalVarName = case is_atom(VarName) of
+                   true ->
+                     atom_to_list(VarName);
+                   false ->
+                     VarName
+                 end,
+  build_bindings(T, [["var ", FinalVarName, "=", mochijson2:encode(Value), ";\n"]|Accum]).
+
+build_arg_list([], Accum) ->
+  lists:reverse(Accum);
+build_arg_list([H|[]], Accum) ->
+  build_arg_list([], [mochijson2:encode(H)|Accum]);
+build_arg_list([H|T], Accum) ->
+  build_arg_list(T, [[mochijson2:encode(H), ","]|Accum]).

apps/erlang_js/src/js_benchmark.erl

+-module(js_benchmark).
+
+-define(COUNTS, [1000, 10000, 100000]).
+
+-export([run/0]).
+
+run() ->
+  application:start(erlang_js),
+  {ok, Ctx} = js_driver:new(),
+  js:define(Ctx, "function add(x, y) { return x + y; }", []),
+  Result = [time_calls(Ctx, Count) || Count <- ?COUNTS],
+  js_driver:destroy(Ctx),
+  Result.
+
+time_calls(Ctx, Count) ->
+  io:format("Starting: ~p~n", [Count]),
+  Start = erlang:now(),
+  do_calls(Ctx, Count),
+  timer:now_diff(erlang:now(), Start) / Count.
+do_calls(_Ctx, 0) ->
+  ok;
+do_calls(Ctx, Count) ->
+  CorrectResult = Count * 2,
+  {ok, CorrectResult} = js:call(Ctx, "add", [Count, Count]),
+  do_calls(Ctx, Count - 1).

apps/erlang_js/src/js_cache.erl

+-module(js_cache).
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/0, store/2, delete/1, fetch/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+         terminate/2, code_change/3]).
+
+-define(SERVER, ?MODULE).
+
+-record(state, {cache=gb_trees:empty()}).
+
+start_link() ->
+  gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
+
+store(Key, Value) ->
+  gen_server:cast(?SERVER, {store, Key, Value}).
+
+delete(Key) ->
+  gen_server:cast(?SERVER, {delete, Key}).
+
+fetch(Key) ->
+  gen_server:call(?SERVER, {fetch, Key}).
+
+init([]) ->
+  {ok, #state{}}.
+
+handle_call({fetch, Key}, _From, #state{cache=Cache}=State) ->
+  Result = case gb_trees:lookup(Key, Cache) of
+             {value, Value} ->
+               Value;
+             Other ->
+               Other
+           end,
+  {reply, Result, State};
+
+handle_call(_Request, _From, State) ->
+  {reply, ignore, State}.
+
+handle_cast({store, Key, Value}, #state{cache=Cache}=State) ->
+  {noreply, State#state{cache=gb_trees:enter(Key, Value, Cache)}};
+
+handle_cast({delete, Key}, #state{cache=Cache}=State) ->
+  {noreply, State#state{cache=gb_trees:delete(Key, Cache)}};
+
+handle_cast(_Msg, State) ->
+  {noreply, State}.
+
+handle_info(_Info, State) ->
+  {noreply, State}.
+
+terminate(_Reason, _State) ->
+  ok.
+
+code_change(_OldVsn, State, _Extra) ->
+  {ok, State}.
+
+%% Internal functions

apps/erlang_js/src/js_driver.erl

+-module(js_driver).
+
+-export([load_driver/0, new/0, new/1, destroy/1, shutdown/1]).
+-export([define_js/2, define_js/3, eval_js/2, eval_js/3]).
+
+-define(SCRIPT_TIMEOUT, 5000).
+-define(DRIVER_NAME, "spidermonkey_drv").
+
+load_driver() ->
+  {ok, Drivers} = erl_ddll:loaded_drivers(),
+  case lists:member(?DRIVER_NAME, Drivers) of
+    false ->
+      case erl_ddll:load(priv_dir(), ?DRIVER_NAME) of
+        ok ->
+          true;
+        _ ->
+          false
+      end;
+    true ->
+      true
+  end.
+
+new() ->
+  {ok, Port} = new(no_json),
+  %% Load json converter for use later
+  case define_js(Port, <<"json2.js">>, json_converter(), ?SCRIPT_TIMEOUT) of
+    ok ->
+      {ok, Port};
+    {error, Reason} ->
+      port_close(Port),
+     {error, Reason}
+  end.
+
+new(no_json) ->
+  {ok, open_port({spawn, ?DRIVER_NAME}, [binary])}.
+
+destroy(Ctx) ->
+  port_close(Ctx).
+
+shutdown(Ctx) ->
+  call_driver(Ctx, "sd", [], 60000),
+  port_close(Ctx).
+
+define_js(Ctx, Js) ->
+  define_js(Ctx, Js, ?SCRIPT_TIMEOUT).
+
+define_js(Ctx, {file, FileName}, Timeout) ->
+  {ok, File} = file:read_file(FileName),
+  define_js(Ctx, list_to_binary(FileName), File, Timeout);
+define_js(Ctx, Js, Timeout) when is_binary(Js) ->
+  define_js(Ctx, <<"unnamed">>, Js, Timeout).
+
+define_js(Ctx, FileName, Js, Timeout) when is_binary(FileName),
+                                           is_binary(Js) ->
+  case call_driver(Ctx, "dj", [FileName, Js], Timeout) of
+    {error, ErrorJson} when is_binary(ErrorJson) ->
+      {struct, [{<<"error">>, {struct, Error}}]} = mochijson2:decode(ErrorJson),
+      {error, Error};
+    Result ->
+      Result
+  end.
+
+eval_js(Ctx, Js) ->
+  eval_js(Ctx, Js, ?SCRIPT_TIMEOUT).
+
+eval_js(Ctx, {file, FileName}, Timeout) ->
+  {ok, File} = file:read_file(FileName),
+  eval_js(Ctx, File, Timeout);
+eval_js(Ctx, Js, Timeout) when is_binary(Js) ->
+  case call_driver(Ctx, "ej", [<<"<unnamed>">>, jsonify(Js)], Timeout) of
+    {ok, Result} ->
+      {ok, mochijson2:decode(Result)};
+    {error, ErrorJson} when is_binary(ErrorJson) ->
+      {struct, [{<<"error">>, {struct, Error}}]} = mochijson2:decode(ErrorJson),
+      {error, Error};
+    Error ->
+      Error
+  end.
+
+%% Internal functions
+jsonify(Code) when is_binary(Code) ->
+  {Body, <<LastChar:8>>} = split_binary(Code, size(Code) - 1),
+  C = case LastChar of
+        $; ->
+          Body;
+        _ ->
+          Code
+      end,
+  list_to_binary([<<"var result; try { result = JSON.stringify(">>, C, <<"); } catch(e) { result = JSON.stringify(e)} result;">>]).
+
+priv_dir() ->
+  filename:join([filename:dirname(code:which(?MODULE)), "..", "priv"]).
+
+call_driver(Ctx, Command, Args, Timeout) ->
+  Marshalled = js_drv_comm:pack(Command, Args),
+  port_command(Ctx, Marshalled),
+  Result = receive
+             Response ->
+               Response
+           after Timeout ->
+               {error, timeout}
+           end,
+  Result.
+
+json_converter() ->
+  FileName = filename:join([priv_dir(), "json2.js"]),
+  case js_cache:fetch(FileName) of
+    none ->
+      {ok, Contents} = file:read_file(FileName),
+      js_cache:store(FileName, Contents),
+      Contents;
+    Contents ->
+      Contents
+  end.

apps/erlang_js/src/js_drv_comm.erl

+-module(js_drv_comm).
+
+-export([pack/2]).
+
+pack(Cmd, Terms) when length(Cmd) == 2 ->
+  NewTerms = lists:foldr(fun(T, Acc) -> marshal_data(T, Acc) end, [], Terms),
+  list_to_binary(lists:flatten([[Cmd], [NewTerms]])).
+
+%% Internal functions
+marshal_data(Term, Acc) when is_integer(Term) ->
+  [<<Term:32>>|Acc];
+marshal_data(Term, Acc) when is_list(Term) ->
+  marshal_data(list_to_binary(Term), Acc);
+marshal_data(Term, Acc) when is_binary(Term) ->
+  S = size(Term),
+  [[<<S:32>>, Term]|Acc].

apps/erlang_js/src/js_memory.erl

+-module(js_memory).
+
+-define(COUNT, 1000000).
+
+-export([stress/1]).
+
+stress(new) ->
+  Start = erlang:memory(total),
+  do(new, ?COUNT),
+  display(end_test() - Start);
+
+stress(error) ->
+  Start = erlang:memory(total),
+  do(error, ?COUNT),
+  display(end_test() - Start).
+
+%% Internal functions
+do(error, 0) ->
+  ok;
+do(error, Count) ->
+  show_count(Count),
+  {ok, P} = js_driver:new(),
+  {error, _Error} = js:define(P, <<"function foo(;">>),
+  js_driver:destroy(P),
+  do(error, Count - 1);
+
+do(new, 0) ->
+  ok;
+do(new, Count) ->
+  show_count(Count),
+  {ok, P} = js_driver:new(),
+  js_driver:destroy(P),
+  do(new, Count - 1).
+
+end_test() ->
+  [erlang:garbage_collect(P) || P <- erlang:processes()],
+  erlang:memory(total).
+
+display(Memory) ->
+  io:format("Used ~p bytes during test.~n", [Memory]).
+
+show_count(Count) ->
+  if
+    (?COUNT - Count) rem 1000 == 0 ->
+      io:format("~p~n", [Count]);
+    true ->
+      ok
+  end.

apps/erlang_js/tests/Emakefile

+{"*", [warn_obsolete_guard, warn_unused_import,
+       warn_shadow_vars, warn_export_vars, debug_info,
+       {i, "../include"},
+       {outdir, "../ebin"}]}.

apps/erlang_js/tests/driver_tests.erl

+-module(driver_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+load_test_() ->
+  [{setup, fun test_util:port_setup/0,
+    fun test_util:port_teardown/1,
+    [fun() ->
+         P = test_util:get_thing(),
+         ?assert(is_port(P)),
+         erlang:unlink(P) end]}].
+
+destroy_test_() ->
+  [{setup, fun test_util:port_setup/0,
+    fun test_util:null_teardown/1,
+    [fun() ->
+         P = test_util:get_thing(),
+         ?assertMatch(true, js_driver:destroy(P)),
+         ?assertError(badarg, js:define(P, <<"var x = 100;">>)),
+         erlang:unlink(P) end]}].

apps/erlang_js/tests/eval_tests.erl

+-module(eval_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+var_test_() ->
+  [{setup, fun test_util:port_setup/0,
+    fun test_util:port_teardown/1,
+    [fun() ->
+         P = test_util:get_thing(),
+         ?assertMatch(ok, js:define(P, <<"var x = 100;">>)),
+         ?assertMatch({ok, 100}, js:eval(P, <<"x;">>)),
+         erlang:unlink(P) end]}].
+
+function_test_() ->
+  [{setup, fun test_util:port_setup/0,
+    fun test_util:port_teardown/1,
+    [fun() ->
+         P = test_util:get_thing(),
+         ?assertMatch(ok, js:define(P, <<"function add_two(x, y) { return x + y; };">>)),
+         ?assertMatch({ok, 95}, js:call(P, <<"add_two">>, [85, 10])),
+         ?assertMatch({ok, <<"testing123">>}, js:call(P, <<"add_two">>, [<<"testing">>, <<"123">>])),
+         erlang:unlink(P) end,
+     fun() ->
+        P = test_util:get_thing(),
+        ?assertMatch(ok, js:define(P, <<"var f = function(x, y) { return y - x; };">>)),
+        ?assertMatch({ok, 75}, js:call(P, <<"f">>, [10, 85])),
+        erlang:unlink(P) end]}].
+
+error_test_() ->
+  [{setup, fun test_util:port_setup/0,
+    fun test_util:port_teardown/1,
+    [fun() ->
+         P = test_util:get_thing(),
+         {error, ErrorDesc} = js:define(P, <<"functoin foo(x, y) { return true; };">>),
+         ?assert(verify_error(ErrorDesc)),
+         erlang:unlink(P) end,
+     fun() ->
+         P = test_util:get_thing(),
+         ?assertMatch(ok, js:define(P, <<"function foo(x, y) { return true; };">>)),
+         {error, ErrorDesc} = js:eval(P, <<"foo(100, 200,);">>),
+         ?assert(verify_error(ErrorDesc)),
+         erlang:unlink(P) end]}].
+
+%% Internal functions
+verify_error([{<<"lineno">>, LineNo},
+              {<<"message">>, Msg},
+              {<<"source">>, Source}]) when is_number(LineNo),
+                                            is_binary(Msg),
+                                            is_binary(Source) ->
+  true;
+verify_error(_) ->
+  false.

apps/erlang_js/tests/test_suite.erl

+-module(test_suite).
+
+-include_lib("eunit/include/eunit.hrl").
+
+all_test_() ->
+  [{module, driver_tests},
+   {module, eval_tests}].

apps/erlang_js/tests/test_util.erl

+-module(test_util).
+
+-export([port_setup/0, port_teardown/1, null_teardown/1, get_thing/0]).
+
+port_setup() ->
+  {ok, P} = js_driver:new(),
+  start_thing_holder(P),
+  P.
+
+port_teardown(P) ->
+  thing_holder ! stop,
+  erlang:port_connect(P, self()),
+  js_driver:destroy(P).
+
+null_teardown(_) ->
+  thing_holder ! stop,
+  ok.
+
+get_thing() ->
+  thing_holder ! {get_thing, self()},
+  receive
+    Thing ->
+      if
+        is_port(Thing) ->
+          erlang:port_connect(Thing, self());
+        true ->
+          ok
+      end,
+      Thing
+  end.
+
+%% Internal functions
+start_thing_holder(Thing) ->
+  if
+    is_port(Thing) ->
+      erlang:unlink(Thing);
+    true ->
+      ok
+  end,
+  Pid = spawn(fun() -> thing_holder(Thing) end),
+  register(thing_holder, Pid),
+  Pid.
+
+thing_holder(Thing) ->
+  receive
+    {get_thing, Caller} ->
+      Caller ! Thing,
+      thing_holder(Thing);
+    stop ->
+      ok
+  end.
 {sub_dirs, ["apps/mochiweb",
 	    "apps/webmachine",
             "apps/riak",
+	    "apps/erlang_js",
             "rel"]}.

rel/reltool.config

          sasl,
          crypto,
          runtime_tools,
+	 erlang_js,
          mochiweb,
          webmachine,
          riak
        {profile, embedded},
        {excl_sys_filters, ["^bin/.*",
                            "^erts.*/bin/(dialyzer|typer)"]},
+       {app, erlang_js, [{incl_cond, include}]},
        {app, riak, [{incl_cond, include}]},
        {app, sasl, [{incl_cond, include}]}
       ]}.
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.