Native Plugins
Rewrite any WebAPI call at native speed with a shared library that a persona names.
A plugin is a shared library built standalone against one header, bauta_plugin.h: a .so on Linux, a .dylib
on macOS, a .dll on Windows. A persona names it, and bauta calls it at the exit of every WebAPI call, the same
point where persona rules apply.
A plugin can compute an answer, choose one from the call's arguments, or rewrite a returned buffer, all in C at
native speed and with no browser rebuild. Persona rules run first; a plugin sees their result and may override it.
The header and a worked example are in the plugins/ directory of the
bauta-browser repository.
The two exported symbols
A plugin exports two symbols:
uint32_t bauta_plugin_init(const BautaHost* host);
void bauta_plugin_transform(BautaCall* call);bauta_plugin_initis called once, at load. Keephost. Return theBAUTA_PLUGIN_ABIyou built with to accept the load, or 0 to decline it.bauta_plugin_transformis called at every call exit. Checkhost->label(call), and return without setting anything for calls you do not handle.
Versions
bauta loads a plugin built for any ABI version up to its own. A plugin should accept a host whose host->abi is at
least the version it was built for, and use a field only if host->abi reaches the version that added it.
What BautaHost provides
| function | purpose |
|---|---|
label(call) | The call's label, as persona rules name it ("Navigator.get platform"). |
argc(call), arg_is_number(call, i), arg_number(call, i) | The call's arguments. |
set_number, set_bool, set_string | Replace the return value. |
set_json(call, json) | Replace it with any JSON value, wrapped in a resolved Promise when the call returns one. |
return_bytes(call, &ptr, &len) | The returned RGBA bytes (getImageData's pixels), to edit in place. |
return_floats(call, &ptr, &count) | The returned Float32Array (getChannelData), to edit in place. |
arg_is_string(call, i), arg_string(call, i, buf, cap) | ABI 2: a string argument, as UTF-8. |
persona(call), seed(call, domain) | ABI 2: the persona's name, and its noise seed for a domain (keyed per install). |
realm(call) | ABI 2: who called: BAUTA_REALM_PAGE, BAUTA_REALM_DEDICATED_WORKER, BAUTA_REALM_SHARED_WORKER, BAUTA_REALM_SERVICE_WORKER or BAUTA_REALM_OTHER. |
set_json answers as a persona rule does: an attribute's array or object is the same object on every read (an
array frozen), and a Promise-returning call gets a resolved Promise.
Build
The header exports the two entry points (BAUTA_PLUGIN_EXPORT), so no linker option is needed. From
plugins/examples:
gcc -shared -fPIC -O2 -I.. sample_plugin.c -o sample.so # Linux
clang -shared -O2 -I.. sample_plugin.c -o sample.dylib # macOS# Windows, in a Visual Studio prompt
cl /LD /MT /O2 /I.. sample_plugin.c /Fe:sample.dllOn Windows a renderer loads only DLLs Microsoft signed, and the persona's plugin, which bauta lets through. So a
plugin is one DLL that depends on nothing but Windows' own DLLs. /MT links the C runtime in; without it the DLL
needs the Visual C++ runtime, which a machine without it lacks.
Name the plugin in a persona
Through the gateway, a persona names its plugin in persona.json:
{"builtin": "seed:windows:shop", "plugin": "/abs/path/sample.so"}A relative path is resolved against the persona file that bauta persona new --from reads. Every page of that
persona runs the plugin, including its workers. Personas that name the same plugin share a browser; personas with
another plugin or none get a browser of their own. A persona whose plugin is missing does not start, and the
gateway says why.
From the packages, pass plugin:
browser = launch(plugin="/abs/path/sample.so")const browser = await launch({ plugin: "/abs/path/sample.so" });A browser launched directly takes BAUTA_PLUGIN=/abs/path/sample.so in its environment.
bauta prints [bauta] native: loaded <path> on stderr when it accepts the plugin.
The example plugin
plugins/examples/sample_plugin.c shows shapes that a declarative rule cannot all express:
- a computed value:
navigator.hardwareConcurrencybecomesfib(7), in the page and in its workers; - a string chosen from an argument: the WebGL vendor and renderer, picked by
getParameter's enum; - an answer chosen from a string argument (ABI 2):
CSS.supports("bauta", …)becomestrue; - an in-place transform of a returned buffer:
getImageDatapixels.
Rules for handlers
- Never throw across the C boundary. A C++ plugin catches everything.
- Do not call back into page APIs. bauta guards one level of re-entry, but a handler should be self-contained.
- Invalid input is ignored and the real answer stays: a null string, unparseable JSON, or a buffer request on a call that returns no buffer.
- A crash inside the plugin crashes its renderer. Plugins are trusted code.
- A page's workers call the same members on other interfaces:
WorkerNavigator.*fornavigator, andOffscreenCanvasRenderingContext2D.*for a worker's canvas. Answer those labels too, or a worker reads the real machine while its page reads the plugin's answer (realmsays which one asked). - A few hot members have a fast path that V8's optimized code takes without the binding's exit:
performance.now(), WebGL draw and uniform calls, canvas path calls. A plugin's answer for one of those is not guaranteed once the page's code is optimized.
The sandbox
A plugin works with the sandbox on:
- on Linux, the zygote maps it before the renderer sandbox engages;
- on Windows, the browser lets it past the renderer's code integrity, and the renderer maps it before its token is lowered;
- on macOS, the renderer's sandbox profile lets it read and map that one file.
The plugin runs as native code inside every page's renderer: load only a plugin you trust as you trust the browser.