Passing Data Across the Bridge
Convert standard and library types across the bridge, share binary data, and pass DOM elements.
On this page
The JavaScript bridge converts values between C++ and JavaScript automatically through a set of standard mappings. You can pass common types like numbers, strings, containers, and structs directly across the bridge without writing manual conversion code.
You can also extend these mappings to support your own types.
Standard Type Mappings
Common C++ types convert to and from JavaScript automatically without conversion code.
These standard mappings apply across the bridge:
- Parameters and return values of exposed C++ functions
- Properties
- Events
- Promise results
- Conversions performed with
js::Value::To<T>()
| C++ | JavaScript | Notes |
|---|---|---|
bool |
boolean | |
Numbers (int, double, ...) |
number | |
std::string |
string | |
std::string_view |
string | As a parameter, valid only during the call (for a js::Task, until the task finishes). |
js::Value |
any value | Passed through unconverted. |
js::null / js::undefined |
null / undefined |
|
std::optional<T> |
T or undefined |
null also converts to std::nullopt. |
std::vector<T> |
Array | JavaScript Arrays only (not typed arrays). |
std::map<std::string, T> |
object | |
std::variant<Ts...> |
any of Ts |
Selected by JavaScript type without coercion. |
See the js::TypeTraits reference for the complete list of built-in conversions.
Type Checking
Conversions from the page are strict about the JavaScript type. If an argument does not match the parameter's type, the call fails with a TypeError and the exposed C++ function never runs. A trailing std::optional parameter becomes an optional argument on the page.
Expose a C++ function that takes standard types and returns a value:
api["attack"] = [](std::string target, double damage,
std::vector<std::string> effects,
std::optional<double> crit) {
return ApplyAttack(target, damage, effects, crit.value_or(1.0));
};
Call the function from page script:
myApp.attack("Vampire", 12, ["burn", "slow"]); // crit is left out
myApp.attack("Vampire", 12, [], 2.5);
myApp.attack("Vampire", "12", []);
// TypeError: myApp.attack(string, number, string[], number): argument 2:
// expected number, got '12'
Structs and Enums
Plain structs and enums convert without extra code. A struct becomes a JavaScript object with one property per field, and an enum becomes a string matching the enumerator's name.
Define a struct and an enum, then expose functions that pass them:
enum class Rarity { Common, Rare, Legendary };
struct Monster {
std::string name;
double damage;
Rarity rarity;
std::vector<std::string> loot;
std::optional<std::string> title;
};
void RegisterMonsterAPI(js::API& api) {
api["spawn"] = [](Monster m) { Log(m.name); };
api["boss"] = [] {
return Monster { "Vampire Lord", 30, Rarity::Legendary, { "gold" } };
};
}
Pass matching objects from page script and receive return values as objects:
myApp.spawn({ name: "Vampire", damage: 12, rarity: "Rare",
loot: ["gold", "fang"] }); // title is optional
const boss = myApp.boss(); // { name: "Vampire Lord", damage: 30, ... }
Struct and Enum Requirements
A struct converts automatically when it meets these conditions:
- Declared outside any function and outside an anonymous namespace
- Contains at most 24 fields
- Contains no C arrays (use
std::vectorinstead) - Has no base classes
- Every field converts across the bridge (
std::optionalfields are optional on the page)
For enums, enumerators with values from 0 to 63 convert by default. Specialize js::EnumRange for values outside that range.
👍 Compiler Requirements
Automatic struct and enum conversion requires Clang, GCC, or MSVC 19.40+ (Visual Studio 2022 version 17.10). On older compilers, specialize
js::TypeTraitsfor the type instead.
Receiving Callbacks from the Page
An exposed C++ function can take a std::function parameter to receive a JavaScript function from the page. This mapping works from the page to native code only— native code cannot return a std::function to the page.
Store the callback to call it later:
std::function<void(double)> on_score;
api["onScore"] = [&](std::function<void(double)> callback) {
on_score = callback;
};
// Later, on the Renderer's thread:
if (on_score)
on_score(1200);
Page script passes a standard function:
myApp.onScore((score) => updateScoreboard(score));
Callbacks passed from the page must follow these rules:
- Calls to the stored callback must run on the Renderer's thread.
- The return type must be
void(failures are dropped) orjs::Result<T>(failures return an error)— any other return type causes a compile error. - If the page closes or navigates away, calls fail safely instead of crashing.
Library Types
The library's own types convert across the bridge anywhere standard types do, including function parameters, return values, and containers.
Including <Ultralight/JS.h> provides conversions for most library types. Only dom::Element requires the separate <Ultralight/dom/JSInterop.h> header— leaving it out causes a compiler error that names the missing header.
| C++ | JavaScript | Notes |
|---|---|---|
String |
string | |
URL |
string | From the page, text that isn't an absolute URL fails the call with a TypeError (code ULJS_BAD_ARG). To the page, an invalid URL converts to null. |
Color |
CSS color text | From the page, text that isn't a valid CSS color fails the call with a TypeError (code ULJS_BAD_ARG). To the page, an unset or invalid Color converts to null. |
RefPtr<Buffer>, std::span<T> |
ArrayBuffer, typed array |
Shares memory without copying (see Binary Data below). |
dom::Element |
The element's JavaScript object | Requires <Ultralight/dom/JSInterop.h> (see DOM Elements below). |
Wrap a type in std::optional (such as std::optional<URL> or std::optional<dom::Element>) to accept null from page script.
Bind functions that take and return library types:
api["open"] = [](URL dest) { Navigate(dest); };
api["theme"] = [](Color accent) { SetAccent(accent); };
api["highlight"] = [](dom::Element el) { el.classList.add("highlight"); };
Page script passes string URLs, CSS color values, and DOM elements:
myApp.open("https://ultralig.ht"); // not an absolute URL = TypeError
myApp.theme("oklch(70% 0.1 250)"); // any CSS color text
myApp.highlight(document.querySelector("#title"));
Binary Data
Binary data crosses between native code and the page without copying, in both directions.
Sharing Bytes with the Page
Pass a Buffer to Context::MakeArrayBuffer() to share native memory with the page as an ArrayBuffer:
RefPtr<Buffer> pixels = Buffer::Create(
pixel_data, pixel_size, nullptr,
[](void* user_data, void* data) { FreePixels(data); });
ctx["pixels"] = ctx.MakeArrayBuffer(pixels);
Page script can wrap the shared buffer in a typed array:
const view = new Uint8Array(pixels); // the same memory, no copy
The Buffer destruction callback runs when the last reference is released by native code or the page. Free the memory in that callback.
Call js::ArrayBuffer::Detach() to take the shared bytes back from the page early.
Detach() returns false if the buffer is locked— a buffer locks when native code reads its bytes back through a pointer, or when the page passes it to an exposed C++ function.
Receiving Bytes from the Page
Expose C++ functions that take RefPtr<Buffer> or std::span to receive binary data from the page:
api["upload"] = [](RefPtr<Buffer> bytes) { Enqueue(bytes); };
api["mix"] = [](std::span<const float> samples) { MixAudio(samples); };
Page script passes an ArrayBuffer or a typed array to those functions:
myApp.upload(fileBytes); // an ArrayBuffer
myApp.mix(new Float32Array([0.1, 0.2])); // a Float32Array
A RefPtr<Buffer> parameter views the page's ArrayBuffer.
A std::span parameter views the elements of a typed array only during the call (for a js::Task, until the task finishes).
Receiving bytes locks the ArrayBuffer for the rest of its life— this applies to both RefPtr<Buffer> parameters and std::span parameters (passing a typed array locks its underlying ArrayBuffer). After that, js::ArrayBuffer::Detach() from native code fails, and the page can't detach the buffer either.
đźš§ Lifetime of Page-Owned Bytes
Bytes owned by the page are freed when the page goes away, even while native code holds a
Bufferover them. Copy bytes that must outlive the page usingBuffer::CreateFromCopy().
DOM Elements
Include <Ultralight/dom/JSInterop.h> to pass DOM elements between native code and page script.
Convert elements and look up document contexts using the interop functions:
#include <Ultralight/dom/JSInterop.h>
dom::Document document(view.get());
js::Context ctx = dom::GetJSContext(document);
dom::Element panel = document.querySelector("#panel");
// Give page script an element:
ctx.GlobalObject()["panel"] = dom::ToJS(ctx, panel);
// Get the element a script returns:
dom::Element focused = dom::FromJS(
ctx.Evaluate("document.activeElement").value_or(js::Value()));
| Function | What It Does |
|---|---|
dom::GetJSContext() |
Returns the js::Context of a document in any frame, subframes included (or an empty context if JavaScript is disabled). |
dom::ToJS() |
Returns the JavaScript object page scripts see for the element (or an empty value if the context belongs to another frame). |
dom::FromJS() |
Returns the dom::Element inside a JavaScript value (or an empty element if the value is not an element). |
See About the DOM API for more on working with dom::Document.
Context Types
Several types across the library are called a context.
| Name | Description |
|---|---|
js::Context |
A page's JavaScript context. js::Context(view) wraps the main frame's context, which stops working when the page navigates away. |
dom::GetJSContext() |
Returns the js::Context of any document, including documents in subframes. |
View::GetJSContext() |
Returns a raw handle to the main frame's context for the C API. In C++, use js::Context(view) instead. |
dd::Context |
The data-binding context. Holds bindings across any number of Views and pages, independent of page JavaScript (see About Data Bindings). |
Your Own Types
The bridge converts only the standard mappings, structs, enums, and library types listed above. To pass a custom value type like an ID or a unit, teach the bridge to convert it (see Custom Type Conversions). For an object with identity and methods, expose it as a class (see Exposing Native Classes).
Class Instances and Ownership
A bound-class instance crosses the bridge as a reference to the native object without copying— the C++ type passed decides who owns it. This covers borrowed raw pointers (T*), shared ownership (RefPtr<T> or std::shared_ptr<T>), unique ownership moved to the page's wrapper (std::unique_ptr<T>), instances that are never destroyed (js::Eternal<T>), and weak holders that lock for each call.
See Exposing Native Classes for the ownership table, canonical holders, and examples.