JavaScript FFI using Spidermonkey
While the language specification does not prescribe a method for foreign function interface, JavaScript was born with the idea of hosting it inside a browser and therefore foreign function interface is essential to get things done.
The JavaScript you know is usually a combination of the language, the Web APIs, and the browser runtime. Other projects use JavaScript as a scripting language and provide a different combination.
Spidermonkey is a JavaScript runtime that is embeddable in C/C++/Rust projects and allows you to interpret JavaScript and define bindings for JavaScript.
In this example we will see how we can instruct Spidermonkey to expose a native function to a script. The prerequisites for this project are some knowledge of C/C++.
Native functions
Native functions are usually written in C/C++ and have the following signature.
bool name(JSContext *ctx, unsigned argc, JS::Value *vp);
In a native function you can use the context of execution to interact with the Spidermonkey library through functions defined in the “jsapi.h” header.
A native function receives the arguments from the JavaScript side
through the vp
argument independently of the number of arguments passed.
The remaining argc
argument is the argument count, how many JavaScript
values where used in the function call. This may be different by the number
of values we require.
There are two ways to bind native functions to Spidermonkey and we will see both. Both go through a 2 step process where we first implement a native function with the correct signature and then we register it with Spidermonkey.
The main difference between the two is the namespace on which we will register them.
Native functions as functions
Step 1
This example shows the definition of a function called log
which is available
globally. The function goes through all the provided arguments and prints them
as strings to the standard output of our program.
#include <jsapi.h> // JS_NewContext
#include <js/Conversions.h> // ToString
namespace builtin {
bool log(JSContext *ctx, unsigned argc, JS::Value *vp) {
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
for (unsigned i = 0; i < argc; i++) {
auto encodedArg = JS_EncodeStringToASCII(ctx, JS::ToString(ctx, args.get(i)));
std::fprintf(stdout, encodedArg.get());
}
std::fprintf(stdout, "\n");
args.rval().setUndefined();
return true;
}
};
Step 2
Step two of our implementation is to make Spidermonkey aware of our
native function. What we mean by this is that our script will be able
to call the function log
and Spidermonkey will then call our native
log
function with the correct arguments and resume the normal control
flow after log
returns.
int main(int argc, const char* argv[]) {
auto diagnosis = JS_InitWithFailureDiagnostic();
if (diagnosis) {
std::fprintf(stderr, diagnosis);
return EXIT_FAILURE;
}
JSContext *ctx = JS_NewContext(JS::DefaultHeapMaxBytes);
JS::InitSelfHostedCode(ctx);
JS::RealmOptions options;
JS::RootedObject global(ctx, JS_NewGlobalObject(ctx, &global::globalClass, nullptr, JS::FireOnNewGlobalHook, options));
JSAutoRealm ar(ctx, global);
+ // define the name log to be available on the global object
+ JS_DefineFunction(ctx, global, "log", &builtin::log, 0, 0);
JS::RootedValue rval(ctx);
JS::OwningCompileOptions compilationOptions(ctx);
JS::EvaluateUtf8Path(ctx, compilationOptions, "js/4.js", &rval);
if (JS_IsExceptionPending(ctx)) {
JS::ExceptionStack exnStack(ctx);
JS::StealPendingExceptionStack(ctx, &exnStack);
JS::ErrorReportBuilder builder(ctx);
builder.init(ctx, exnStack, JS::ErrorReportBuilder::NoSideEffects);
JS::PrintError(ctx, stderr, builder, false);
}
JS_DestroyContext(ctx);
JS_ShutDown();
}
Native functions as methods
Step 1
This example shows the definition of a console like object. If you have used JavaScript in the browser before the console is one of the builtin objects that the browser exposes to you. With the console you can print messages that will be available in the browser’s developers tool.
In this example we will instead output those messages to the standard output of our program.
#include <jsapi.h> // JS_NewContext
#include <js/Conversions.h> // ToString
namespace builtin {
namespace console {
// describe the console object to spidermonkey
static JSClass klass = {
"console",
0,
};
// function signature
bool consoleLog(JSContext *ctx, unsigned argc, JS::Value *vp);
// describe the console object methods
static JSFunctionSpec methods[] = {
// native function consoleLog will be exposed as method call .log
// it takes 1 argument
// flag argument is 0
JS_FN("log", consoleLog, 1, 0),
JS_FS_END,
};
// finally implements the native function
bool consoleLog(JSContext *ctx, unsigned argc, JS::Value *vp) {
// get the call arguments
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
for (unsigned i = 0; i < argc; i++) {
auto encodedArg = JS_EncodeStringToASCII(ctx, JS::ToString(ctx, args.get(i)));
std::fprintf(stdout, encodedArg.get());
}
std::fprintf(stdout, "\n");
args.rval().setUndefined();
return true;
}
};
};
Step 2
Step two of our implementation is to make Spidermonkey aware of our
native function. What we mean by this is that our scripts will be able to
call the method log
of the console
object and Spidermonkey will
then call our consoleLog
function with the correct arguments and resume the
normal control flow after consoleLog
returns.
Instead of defining one name log
and bind it to a our function immediately
we go through an intermediate name: console
. The main reason motivating this
choice is familiarity. Developers writing JavaScript are already using the browser’s
console methods and we want to offer the same interface here.
To obtain this behavior we first get a JavaScript object from Spidermonkey and attach it to our console class definition and our console class methods.
#include <js/Initialization.h> // JS_InitWithFailureDiagnostic, JS_Shutdown
#include <jsapi.h> // JS_NewContext
#include <js/CompilationAndEvaluation.h> // EvaluateUtf8Path
#include <js/CompileOptions.h> // OwningCompileOptions
namespace global {
static JSClass globalClass = {
"global",
JSCLASS_GLOBAL_FLAGS,
&JS::DefaultGlobalClassOps,
};
};
int main(int argc, const char* argv[]) {
auto diagnosis = JS_InitWithFailureDiagnostic();
if (diagnosis) {
std::fprintf(stderr, diagnosis);
return EXIT_FAILURE;
}
JSContext *ctx = JS_NewContext(JS::DefaultHeapMaxBytes);
JS::InitSelfHostedCode(ctx);
JS::RealmOptions options;
JS::RootedObject global(ctx, JS_NewGlobalObject(ctx, &global::globalClass, nullptr, JS::FireOnNewGlobalHook, options));
JSAutoRealm ar(ctx, global);
+ // define the name `console` in this context and link its
+ // class to our console implementation
+ JS::RootedObject console(ctx, JS_DefineObject(ctx, global, "console", &builtin::console::klass, 0));
+ // on the object we can now define the methods
+ JS_DefineFunctions(ctx, console, builtin::console::methods);
JS::RootedValue rval(ctx);
JS::OwningCompileOptions compilationOptions(ctx);
JS::EvaluateUtf8Path(ctx, compilationOptions, "js/2.js", &rval);
if (JS_IsExceptionPending(ctx)) {
JS::ExceptionStack exnStack(ctx);
JS::StealPendingExceptionStack(ctx, &exnStack);
JS::ErrorReportBuilder builder(ctx);
builder.init(ctx, exnStack, JS::ErrorReportBuilder::NoSideEffects);
JS::PrintError(ctx, stderr, builder, false);
}
JS_DestroyContext(ctx);
JS_ShutDown();
}