Commits

Anonymous committed d737179 Merge

merge

Comments (0)

Files changed (9)

apps/erlang_js/Makefile

 
 docs: all
 	@mkdir -p docs
-	@ERL_LIBS=`cd ..;pwd`;erl -noshell -eval 'edoc:application(erlang_js, [{dir, "docs"}]), init:stop().'
+	@./build_docs.sh

apps/erlang_js/README

+erlang_js has only been tested on Unix-based platforms (Linux, OS X, and Solaris). Please do not try
+to compile and/or use erlang_js on Windows. It will not work.
+
+We've tried to keep the compile process simple and fast. You will need the CVS command-line client
+installed in order for the compile to work. To compile erlang_js issue the command 'make' in a
+terminal. The build will download and build nspr and SpiderMonkey. It will also build the
+linked-in driver, spidermonkey_drv, and the Erlang source.
+
+You can test the freshly build code via 'make test' which should report all tests passed if the
+build was successful.

apps/erlang_js/build_docs.sh

+#!/bin/bash
+
+export ERL_LIBS=`cd ..;pwd`;
+erl -noshell -eval 'edoc:application(erlang_js, [{dir, "docs"}]), init:stop().'

apps/erlang_js/overview.edoc

+@author Kevin Smith (ksmith@basho.com)
+@version 0.1
+@title erlang_js: A Friendly Erlang to Javascript Binding
+
+@doc
+<p>
+<h3>Description</h3>
+erlang_js aims to be a simple and easy to use binding between Erlang and Javascript. erlang_js is packaged as an OTP application so it's easy to integrate.
+</p>
+
+<p>
+<h3>Quick N' Dirty</h3>
+<ol>
+<li>Make sure the <code>sasl</code> application is running.</li>
+<li>Start <code>erlang_js</code>.</li>
+<li>Create a Javascript VM via <code>js_driver:new/0</code>.</li>
+<li>Use the <code>js</code> module to execute Javascript.</li>
+</ol>
+</p>
+
+<h3>Examples</h3>
+<h4>Defining and calling a function</h4>
+<pre>
+{ok, Port} = js_driver:new().
+ok = js:define(Port, &lt;&lt;"function my_add(x, y) { return x + y; }"&gt;&gt;).
+js:call(Port, &lt;&lt;"my_add"&gt;&gt;, [100, 50]).
+{ok, 150}
+</pre>
+<h4>Using a user-defined initializer</h4>
+<pre>
+InitFun = fun(Port) -> js:define(Port, &lt;&lt;"function my_square(x) { return x * x; }"&gt;&gt;), ok end.
+{ok, Port} = js_driver:new(InitFun).
+js:call(Port, &lt;&lt;"my_square"&gt;&gt;, [3]).
+{ok,9}
+</pre>
+<h4>Using bindings</h4>
+<pre>
+InitFun = fun(Port) -> js:define(Port, &lt;&lt;"var my_constant = 100; function constant_mult(x) { return x * my_constant; };"&gt;&gt;), ok end.
+{ok, Port} = js_driver:new(InitFun).
+js:call(Port, &lt;&lt;"constant_mult"&gt;&gt;, [5]).
+{ok, 500}
+js:call(Port, &lt;&lt;"constant_mult"&gt;&gt;, [5], [{&lt;&lt;"my_constant"&gt;&gt;, 200}]).
+{ok, 1000}
+</pre>

apps/erlang_js/src/js.erl

 %%    See the License for the specific language governing permissions and
 %%    limitations under the License.
 
+%% @doc Convenience module for interacting with Javascript from Erlang.
+%% The functions provided by this module marshal bindings and function
+%% args into JSON before sending them to Javascript. While this does
+%% incur a certain amount of overhead it has the benefit of (mostly)
+%% preserving types as they roundtrip between Erlang and Javascript.
+%% Of course, this also means all Erlang values MUST BE convertable
+%% into JSON. In practice, this is less restricting than it sounds.
 -module(js).
 
 -export([define/2, define/3, eval/2, call/3, call/4]).
 
+%% @spec define(port(), binary()) -> ok | {error, any()}
+%% @doc Define one or more Javascript expressions.
 define(Ctx, Js) ->
     define(Ctx, Js, []).
 
+%% @spec define(port(), binary(), list(any())) -> ok | {error, any()}
+%% @doc Define one or more Javascript expressions using a set of bindings. Bindings
+%% are useful when the expressions use closures.
 define(Ctx, Js, Bindings) ->
     JsBindings = list_to_binary(build_bindings(Bindings, [])),
     FinalJs = iolist_to_binary([JsBindings, Js]),
     js_driver:define_js(Ctx, FinalJs).
 
+%% @spec eval(port(), binary()) -> {ok, any()} | {error, any()}
+%% @doc Evaluate one or more Javascript expressions and return the results
 eval(Ctx, Js) ->
     js_driver:eval_js(Ctx, Js).
 
+%% @spec call(port(), binary(), list(any())) -> {ok, Result} | {error, any()}
+%% @doc Call a function by name with a list of arguments. This is roughly the
+%% same as apply in most other languages.
 call(Ctx, FunctionName, Args) ->
     call(Ctx, FunctionName, Args, []).
 
+%% @spec call(port(), binary(), list(any()), list(any())) -> {ok, Result} | {error, any()}
+%% @doc Call a function by name with a list of arguments and environmental bindings. Bindings
+%% behave just like define/3.
 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 = iolist_to_binary([<<"function() {">>, JsBindings, <<"return ">>, FunctionName, <<"(">>, ArgList, <<");">>, <<"}();">>]),
     js_driver:eval_js(Ctx, Js).
 
 %% Internal functions
                        false ->
                            VarName
                    end,
-    build_bindings(T, [["var ", FinalVarName, "=", js_mochijson2:encode(Value), ";\n"]|Accum]).
+    build_bindings(T, [[FinalVarName, "=", js_mochijson2:encode(Value), ";"]|Accum]).
 
 build_arg_list([], Accum) ->
     lists:reverse(Accum);

apps/erlang_js/src/js_benchmark.erl

 run() ->
     application:start(erlang_js),
     {ok, Ctx} = js_driver:new(),
-    js:define(Ctx, "function add(x, y) { return x + y; }", []),
+    js:define(Ctx, <<"function add(x, y) { return x + y; }">>, []),
     Result = [time_calls(Ctx, Count) || Count <- ?COUNTS],
     js_driver:destroy(Ctx),
     Result.

apps/erlang_js/src/js_cache.erl

 start_link() ->
     gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
 
-%% @spec store(any(), any() -> ok
+%% @spec store(any(), any()) -> ok
 %% @doc Store a key/value pair
 store(Key, Value) ->
     gen_server:cast(?SERVER, {store, Key, Value}).

apps/erlang_js/src/js_driver.erl

 %%    See the License for the specific language governing permissions and
 %%    limitations under the License.
 
+%% @doc This module manages all of the low-level details surrounding the
+%% linked-in driver. It is reponsible for loading and unloading the driver
+%% as needed. This module is also reponsible for creating and destroying
+%% instances of Javascript VMs.
+
 -module(js_driver).
 
 -export([load_driver/0, new/0, new/1, destroy/1, shutdown/1]).
 -define(SCRIPT_TIMEOUT, 5000).
 -define(DRIVER_NAME, "spidermonkey_drv").
 
+%% @spec load_driver() -> true | false
+%% @doc Attempt to load the Javascript driver
 load_driver() ->
     {ok, Drivers} = erl_ddll:loaded_drivers(),
     case lists:member(?DRIVER_NAME, Drivers) of
             case erl_ddll:load(priv_dir(), ?DRIVER_NAME) of
                 ok ->
                     true;
-                _ ->
+                {error, Error} ->
+                    error_logger:error_msg("Error loading ~p: ~p~n", [?DRIVER_NAME, erl_ddll:format_error(Error)]),
                     false
             end;
         true ->
             true
     end.
 
+%% @spec new() -> {ok, port()} | {error, atom()} | {error, any()}
+%% @doc Create a new Javascript VM instance and preload Douglas Crockford's
+%% json2 converter (http://www.json.org/js.html)
 new() ->
     {ok, Port} = new(no_json),
     %% Load json converter for use later
             {error, Reason}
     end.
 
+%% @type init_fun() = function(port()).
+%% @spec new(no_json | init_fun() | {ModName::atom(), FunName::atom()}) -> {ok, port()} | {error, atom()} | {error, any()}
+%% @doc Create a new Javascript VM instance. The function arguments control how the VM instance is initialized.
+%% User supplied initializers must return true or false.
 new(no_json) ->
     {ok, open_port({spawn, ?DRIVER_NAME}, [binary])};
 new(Initializer) when is_function(Initializer) ->
         ok ->
             {ok, Port};
         {error, Error} ->
+            js_driver:destroy(Port),
             error_logger:error_report(Error),
             throw({error, init_failed})
     end;
         ok ->
             {ok, Port};
         {error, Error} ->
+            js_driver:destroy(Port),
             error_logger:error_report(Error),
             throw({error, init_failed})
     end.
 
+%% @spec destroy(port()) -> ok
+%% @doc Destroys a Javascript VM instance
 destroy(Ctx) ->
     port_close(Ctx).
 
+%% @spec shutdown(port()) -> ok
+%% @doc Destroys a Javascript VM instance and shuts down the underlying Javascript infrastructure.
+%% NOTE: No new VMs can be created after this call is made!
 shutdown(Ctx) ->
     call_driver(Ctx, "sd", [], 60000),
     port_close(Ctx).
 
+%% @spec define_js(port(), binary()) -> ok | {error, any()}
+%% @doc Define a Javascript expression:
+%% js_driver:define(Port, &lt;&lt;"var x = 100;"&gt;&gt;).
 define_js(Ctx, Js) ->
     define_js(Ctx, Js, ?SCRIPT_TIMEOUT).
 
+%% @private
 define_js(Ctx, {file, FileName}, Timeout) ->
     {ok, File} = file:read_file(FileName),
     define_js(Ctx, list_to_binary(FileName), File, Timeout);
             ok
     end.
 
+%% @spec eval_js(port(), binary()) -> {ok, any()} | {error, any()}
+%% @doc Evaluate a Javascript expression and return the result
 eval_js(Ctx, Js) ->
     eval_js(Ctx, Js, ?SCRIPT_TIMEOUT).
 
+%% @private
 eval_js(Ctx, {file, FileName}, Timeout) ->
     {ok, File} = file:read_file(FileName),
     eval_js(Ctx, File, Timeout);
     end.
 
 %% Internal functions
+%% @private
 jsonify(Code) when is_binary(Code) ->
     {Body, <<LastChar:8>>} = split_binary(Code, size(Code) - 1),
     C = case LastChar of
         end,
     list_to_binary([<<"var result; try { result = JSON.stringify(">>, C, <<"); } catch(e) { result = JSON.stringify(e)} result;">>]).
 
+%% @private
 priv_dir() ->
     %% Hacky workaround to handle running from a standard app directory
     %% and .ez package
             Dir
     end.
 
+%% @private
 call_driver(Ctx, Command, Args, Timeout) ->
     CallToken = make_call_token(),
     Marshalled = js_drv_comm:pack(Command, [CallToken] ++ Args),
              end,
     Result.
 
+%% @private
 make_call_token() ->
     list_to_binary(integer_to_list(erlang:phash2(erlang:make_ref()))).
 
+%% @private
 json_converter() ->
     FileName = filename:join([priv_dir(), "json2.js"]),
     case js_cache:fetch(FileName) of

apps/erlang_js/tests/eval_tests.erl

                ?assertMatch({ok, <<"abc">>}, js:call(P, <<"get_first">>, [Data])),
                erlang:unlink(P) end]}].
 
+binding_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 c = 100;function constant_mult(x) { return x * c; }">>)),
+               ?assertMatch({ok, 200}, js:call(P, <<"constant_mult">>, [2])),
+               ?assertMatch({ok, 1000}, js:call(P, <<"constant_mult">>, [2], [{<<"c">>, 500}])),
+               erlang:unlink(P) end,
+       fun() ->
+               P = test_util:get_thing(),
+               ?assertMatch(ok, js:define(P, <<"function constant_div(x) { return x / q; }">>, [{<<"q">>, 2}])),
+               ?assertMatch({ok, 5}, js:call(P, <<"constant_div">>, [10])),
+               ?assertMatch({ok, 3}, js:call(P, <<"constant_div">>, [9], [{<<"q">>, 3}])),
+               erlang:unlink(P) end]}].
+
 json_test_() ->
   [fun() ->
        Struct = {struct, [{<<"test">>, <<"1">>}]},