Extending JavaScript with Native API
Expose native data and functions to JavaScript via native API objects.
On this page
The JavaScript language itself only covers basics like variables, functions, objects, and built-in types like arrays and promises. Web APIs like window, document, and console aren't part of the core language at all— browsers add them on top so that scripts can interact with the page and the rest of the browser.
Your application can do the same thing— by defining native API, you can give JavaScript access to a subset of your application's functions and data— and scripts can use them just like they would any other built-in Web API.
Creating an API
A js::API allows you to expose native functions, properties, and classes to JavaScript under a global, frozen API interface.
// Create a JavaScript API object called "app" that will be accessible to
// page scripts under a frozen (read-only), global interface named "app".
js::API app("app");
// Add some properties:
app["version"] = "2.1.0"; // app.version
app["quit"] = js::Bind(&app_, &App::Quit); // app.quit()
Binding Functions and Values
You add properties to an API object via operator[]:
app["version"] = "2.1.0"; // a constant
app["add"] = [](double a, double b) { return a + b; }; // a function
app["quit"] = js::Bind(&app_, &App::Quit); // a member function
app["fs.readFile"] = [](std::string path) { // a nested path
return ReadFile(path);
};
JavaScript can later access these via the "app" object:
console.log(app.version); // "2.1.0"
console.log(app.add(2, 3)); // returns 5
const text = app.fs.readFile("save.dat");
app.quit();
Plain values (like app["version"] = "2.1.0") become read-only constants (see Live Properties below for true getters and setters).
A dot in a path string (like fs.readFile) creates a nested API (app.fs).
Binding properties again replaces what was there.
Parameters and return values are converted to C++ types (see Passing Data Across the Bridge for what converts).
Attaching to a View
You'll need to explicitly attach any native API objects to a View before using it on a page:
if (app.AttachTo(view.get()))
view->LoadURL("file:///app.html");
Every page the View loads from then on gets the API, so you don't need to bind again after a navigation.
Pass js::AttachOptions as an optional second argument to configure attach flags and origin rules (see below).
Keeping the API Alive
You should store the js::API as a class member to keep it alive and bind member functions:
class App {
public:
explicit App(View* view) {
api_["quit"] = js::Bind(this, &App::Quit);
if (api_.AttachTo(view))
view->LoadURL("file:///app.html");
}
void Quit();
private:
js::API api_{"app"}; // detaches from every View when the App goes away
};
A View keeps the API attached only while the js::API instance exists. Keep the object alive for as long as pages need the bindings.
The js::API does not need to outlive the View— destroying it earlier is safe. Destroying it detaches the API from every View. Events stop reaching the page, and any functions or class constructors that the page still holds throw a TypeError with code ULJS_DETACHED.
Bound callables and anything they capture are destroyed on the Renderer's thread. Any waiting js::Task also stops and rejects its Promise with ULJS_DETACHED (see Async Callbacks).
What You Can Bind
The C++ type on the right-hand side decides what the page gets:
| What You Bind in C++ | What the Page Gets |
|---|---|
A callable (lambda, free function, or js::Bind() member) |
A function |
| A value (number, string, bool, vector, or struct) | A constant (copied when you bind it) |
A getter and optional setter bound with BindProperty() |
A live property |
A callable taking a trailing js::Resolver or returning js::Task<T> |
An async function that returns a Promise (Async Callbacks) |
A class defined with DefineClass<T>() |
A class page script can create with new (Exposing Native Classes) |
Live Properties
You can use API::BindProperty() to declare getters and setters for a property (leave out the setter to make it read-only):
app.BindProperty("volume", [] { return g_volume; },
[](double v) { g_volume = v; });
console.log(app.volume); // calls the getter
app.volume = 0.5; // calls the setter
Naming Parameters
To make debugging easier, you can optionally name parameters for more descriptive JavaScript errors.
Use Bind() instead of assignment to add the names:
app.Bind("setConfig", [](double volume, bool muted) { /* ... */ },
js::Param("volume"), js::Param("muted"),
js::Doc("Applies the audio settings."));
The JavaScript TypeError will now include the parameter name:
app.setConfig("loud", true);
// TypeError: app.setConfig(volume: number, muted: boolean):
// argument 1 (volume): expected number, got 'loud'
If you name one parameter, you should name them all. The optional js::Doc also adds a description to the API's schema (see API Schemas and TypeScript).
Throwing JavaScript Errors from Native Code
Return a js::Result holding a js::Error to throw JavaScript errors:
app["open"] = [](std::string path) -> js::Result<double> {
if (path.empty())
return js::Unexpected(js::Error::TypeError("path must not be empty")
.WithCode("APP_BAD_PATH"));
return 1.0;
};
try {
app.open('');
} catch (e) {
if (e.code === 'APP_BAD_PATH')
console.log(e.message); // "path must not be empty"
}
WithCode() sets the error's code property, so page script can check what went wrong without parsing the message.
See JavaScript Errors and Diagnostics for error types, the library's built-in error codes, and diagnostics.
Choosing Which Pages Get the API
By default only local pages (eg, file:// and View::LoadHTML()) can access native API objects, remote pages (eg, http://) will not be able to access the API unless you extend the origin rules.
Origin Rules
To let remote pages access native API, pass origin rules to AttachTo():
std::vector<const char*> rules = { "https://*.mygame.com", "file://*" };
app.AttachTo(view.get(), { .origin_rules = rules });
Each rule has the form scheme://host[:port]:
| Part | Accepts |
|---|---|
| Scheme | An exact scheme (required, no wildcard) |
| Host | An exact host, * for any host, or *.example.com for the domain and all its subdomains |
| Port | Optional— no port matches the scheme's default port, :8443 matches that port, and :* matches any port |
🚧 Origin Rules Replace Defaults
Passing origin rules replaces the default access entirely, so only matching pages get the API. Include
"file://*"to keep access for local pages.View::LoadHTML()uses the URL you provide as the page's origin, but omitting the URL gives the page an opaque origin that no rule matches— use an injection filter to allow it instead.
Filters
For decisions origin rules can't express (like a runtime user setting), set a filter on the View:
js::SetInjectionFilter(view.get(), [](const js::InjectionRequest& request) {
return request.rules_allow && request.is_main_frame;
});
The View calls the filter each time it is about to add an attached API's bindings to a page (usually after each navigation)
Origin rules will still be evaluated first— their verdict will be stored in rules_allow in the injection filter's request param.
Return true to add the API bindings to a page (or false to withhold them).
Subframes and Frozen APIs
Set flags in the attach options:
| Flag | Effect |
|---|---|
js::AllFrames |
Also adds the API object to subframes (by default only the main frame gets them) |
js::Mutable |
Allows page script to modify the native API object (by default they're frozen) |
app.AttachTo(view.get(), { .flags = js::AllFrames | js::Mutable });
Changing Bindings Later
You can add, replace, or remove bindings after attaching. Each page sees the change at its next navigation.
🚧 Bind on the Renderer's Thread
Bind from one thread (usually at startup). Once the API is attached, bind and unbind only on the Renderer's thread.
Removing a Binding
Call Unbind() with the path:
app.Unbind("debug.dump");
Hold a js::BindingGuard to remove a binding when a C++ object goes away:
app.Bind("debug.dump", [] { DumpState(); });
js::BindingGuard dump_guard(app, "debug.dump"); // unbinds when destroyed
Detaching from a View
Call DetachFrom() to take the whole API off a View:
app.DetachFrom(view.get());
Events stop reaching the View's pages right away. The current page keeps its bindings until it navigates.