bautaDocs

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_init is called once, at load. Keep host. Return the BAUTA_PLUGIN_ABI you built with to accept the load, or 0 to decline it.
  • bauta_plugin_transform is called at every call exit. Check host->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

functionpurpose
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_stringReplace 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.dll

On 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.hardwareConcurrency becomes fib(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", …) becomes true;
  • an in-place transform of a returned buffer: getImageData pixels.

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.* for navigator, and OffscreenCanvasRenderingContext2D.* for a worker's canvas. Answer those labels too, or a worker reads the real machine while its page reads the plugin's answer (realm says 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.

Next

On this page