Binding Threads and Lifetime
Coordinate data across threads, manage binding lifetime, and attach contexts to views.
On this page
A Context belongs to the thread that creates it, called its home thread. You run most binding calls on this home thread, though a few operations are safe from any thread.
The Home Thread
The thread that calls Context::Create() is the home thread for that Context. You bind instances, register handlers, and change bound data on this thread. Calls to Sync() run here as well.
In a game, native code typically creates the Context on the game thread rather than the Renderer's thread— using the player type from Binding Lists:
void GameThread(View* view) {
Player player;
dd::Context ctx = dd::Context::Create(); // this thread is the home thread
// This instance will be exposed to markup as "player.<field>..."
dd::Binding binding = ctx.Bind("player", player);
if (!ctx.AttachTo(view))
Log("couldn't attach the data context");
while (running) {
UpdateGame(player); // change bound data here, between Syncs
ctx.Sync();
}
}
Thread Roles
Binding operations divide across three thread roles:
| Thread | What Runs There |
|---|---|
| Home | Bind(), Sync(), registering handlers, modifying bound data, and handlers during Sync(). |
| Any | PostAction(), PostTask(), AttachTo(), DetachFrom(), and destroying a Binding. |
| Renderer's | Formatters and validators (which must not access native objects). |
In a single-threaded application, one thread performs all three roles.
🚧 Change Bound Data Only on the Home Thread
Sync()reads bound data without taking locks. Only modify bound instances on the home thread, between calls toSync().
Writing from Several Threads
When multiple worker threads need to modify bound data, give each writer thread its own Context.
Thread-Safe Contexts
If the thread that calls Sync() changes over time (such as across worker threads in a job system), create a thread-safe Context instead. Any thread can then bind data, register handlers, and call Sync(). The Context serializes these calls, and handlers run on whichever thread called Sync().
Create a thread-safe Context with Context::CreateThreadSafe():
dd::Context ctx = dd::Context::CreateThreadSafe();
🚧 Thread-Safe Contexts Don't Protect Your Data
A thread-safe Context coordinates its own calls, but it does not lock bound native instances. Never modify an instance while another thread might be calling
Sync(). A handler that blocks on another thread waiting for this Context will deadlock.
What a Binding Holds
A Binding can hold native data by reference, by value, or through a smart pointer:
Player player;
auto ally = std::make_shared<Player>();
dd::Binding b1 = ctx.Bind("player", player); // borrows player
dd::Binding b2 = ctx.Bind("preview", Player {}); // owns its own Player
dd::Binding b3 = ctx.Bind("ally", ally); // shares ownership of *ally
| Form | Owner | Use For |
|---|---|---|
| Reference | Native code | The common case. Native code must keep the instance alive while bound. |
| Value | The Binding |
Data built once. Member-function handlers are the only way to change it after binding. |
| Smart pointer | Shared | Data with its own lifetime. An owning pointer (std::shared_ptr, RefPtr, or a std::unique_ptr you move in) keeps the instance alive. An expired weak pointer (std::weak_ptr or WeakPtr) stops updates and leaves the last values on the page. |
Keeping the Binding
Destroying a Binding unbinds the instance and removes its handlers. Store the Binding handle alongside the data it binds (such as in a class member):
class Hud {
public:
explicit Hud(dd::Context& ctx) : binding_(ctx.Bind("player", player_)) {}
private:
Player player_;
dd::Binding<Player> binding_; // unbinds when the Hud is destroyed
};
🚧 Keep the Binding
Always store the returned
Bindinghandle. Discarding the result ofContext::Bind()unbinds the instance immediately.
Checking Whether a Binding Still Works
Binding the same name again replaces the earlier binding, and destroying the Context ends all its bindings. The old Binding handle remains safe to use, but its handlers are dropped and PostAction() returns false.
Call IsAlive() to check whether a binding still works:
dd::Binding hud = ctx.Bind("player", player);
dd::Binding menu = ctx.Bind("player", player); // replaces hud's binding
if (!hud.IsAlive())
Log("hud's binding was replaced");
🚧 Check IsAlive Rather Than the Handle
A replaced
Bindingstill holds its handle, so checkingif (binding)evaluates totrue. CallIsAlive()instead to see if the binding is active.
Empty Bindings
Call IsEmpty() to check whether a Binding has no handle (such as after moving from it or when Bind() fails). Destroying a Context stops the library from reading the bound instance, but the Binding still keeps its handle— IsEmpty() stays false, while IsAlive() returns false.
A binding name must start with a letter or an underscore, followed by letters, digits, or underscores (such as hud or main_menu). If a name uses other characters, Bind() logs a warning and returns an empty Binding.
Unbinding from Another Thread
You can destroy a Binding from any thread. On the home thread, unbinding happens immediately. From any other thread, the unbind takes effect at the start of the next Sync(), and that binding receives no input during that cycle.
Attaching to Views
Call AttachTo() from any thread to connect a Context to a View. Every page the View loads receives the bindings automatically without re-attaching on navigation (including pages restored from the back-forward cache).
Contexts attach to Views. A single Context can attach to multiple Views, and one View can hold bindings from multiple Contexts.
A single call to Sync() updates all attached Views at once:
bool ok = ctx.AttachTo(hud_view.get());
ok = ctx.AttachTo(map_view.get()) && ok; // one Sync() updates both Views
Pages from Other Origins
By default, only local file:// pages and pages loaded with View::LoadHTML() receive bindings. To allow pages from other origins, pass origin match patterns to AttachTo() using the syntax described in Extending JavaScript with Native API.
Custom origin rules replace the default policy entirely. If local files still need access to bindings, you'll need to include file://* in your rules.
When you call View::LoadHTML() with a URL, the page uses that URL's origin. Custom rules match it like any other page. Calling View::LoadHTML() without a URL gives the page an opaque origin— no custom rule can match it, so that page receives no bindings (only the default policy allows them).
Pass origin patterns when attaching— AttachTo() returns false if any rule fails to parse:
bool ok = ctx.AttachTo(view.get(), {
.origin_rules = { "https://*.mygame.com", "file://*" } });
Detaching
Call DetachFrom() from any thread to detach a Context from a View:
ctx.DetachFrom(view.get());
During a later update, the page rebuilds its bindings from any Contexts that remain attached.
When no Contexts remain attached, the page reverts to its original authored markup— template elements like ul-if and raw {{path}} expressions return, and bound lists clear their rows. Any values previously set on classes, attributes, styles, or ul-text elements remain on the page.
Destroying a Context
A View keeps a Context attached until native code calls DetachFrom() or destroys the Context.
Destroying a Context detaches it from every View and immediately tears down its bindings, handlers, and formatters on the calling thread. Always destroy a Context on its home thread so this cleanup happens on the thread that created them.
A Binding that outlives its Context remains safe to use. The library stops reading the bound instance, and IsAlive() returns false.
Running Code After the Page Updates
Call PostTask() from any thread to run a function on the Renderer's thread once attached pages display the latest synchronized data:
player.items.push_back({ .id = 7, .name = "Shield" });
ctx.Sync();
// Runs on the Renderer's thread once the page shows the new row.
ctx.PostTask([](dom::Document document) {
document.querySelector("#inventory li:last-of-type").scrollIntoView();
});
The callback's parameters determine how many times it runs:
| Parameters | How Often It Runs |
|---|---|
| No parameters | Runs once after every attached View displays the data. If no View is attached, it runs during a later Renderer update without waiting for a page. |
dom::Document |
Runs once for each attached View and receives that View's document. If no View is attached, it runs once with an empty dom::Document. |
A View whose page is still loading delays the task until that page displays the data— pages that don't use this Context (such as those disallowed by origin rules) never delay it.
When called from a handler during Sync(), the task runs after the current synchronization completes. When called from elsewhere, it runs after the previous Sync().
Always capture variables by value because the callable runs asynchronously on another thread.