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();
 }