A TypeScript core reaches the host with Cmd.request(name, ...), which routes through runtime.effects.HostCallBinding. HostCallBinding is a public type and bindHostCalls() is public runtime API, but no generated path ever installs an app-authored binding — every generated entry point either wires the service carriers or null, and null rejects all non-reserved host requests (src/runtime/effects.zig: if (!fake and self.host_calls == null ...) return self.rejectStartedHost(...)).
So for the SDK's default authoring path, apps whose effects go beyond the built-in command set — custom IPC to a native daemon/service, BLE/hardware, proprietary vendor SDKs — cannot route commands into native code at all. The src/services escape hatch doesn't apply: services are TypeScript and can never reach C FFI.
What each target does today
-
Desktop: the generated runner hardcodes the choice (
src/app_runner/ts_core_main.zig):.host_calls = if (comptime use_pool) pool_transport.binding() else if (comptime use_child) child_transport.binding() else null, // no app-authored binding possibleReal apps doing native effects work around this by patching the generated runner in zig-cache after the build stages it (add an
@importof a host module, wire itsbinding()in). Undocumented and fragile, but possible. -
Mobile: the staged mobile wiring (
src/app_runner/ts_core_mobile.zig) setshost_callsfrom the in-process service pool only. There is no generated file an app can plausibly patch — it stages fresh from the SDK template on every build — so this is impossible, not just undocumented. -
The C ABI shows the intended pattern but stops short:
native_sdk_app_set_audio_service/_set_credential_service/_set_image_serviceare native-side registration seams, but there is no host-call equivalent.bindHostCalls()is only reachable by an embedder that owns the runtime loop — and the iOS tier deliberately owns it (src/tooling/ios.zig: "the toolkit owns the entire iOS app"), locking embedders out of the one API that would solve this. -
A Zig core avoids the problem by construction (its logic is native code and calls FFI directly), which makes the gap specific to TS cores — the default path.
Notes / proposal
A small symmetric seam in both generated runners. We run this today (against 0.10.1) on mobile and are happy to turn it into a PR:
AppOptions.mobile_host(or a manifest declaration — may be cleaner): a module exposingbinding()with the same contract as the desktop runner'shost_callsvalue, plus its include dirs / object files.- The build graph imports it into the staged mobile wiring as
app_host; the wiring references it under a comptime flag so apps without it never touch the import. Implementation note from doing this: the generated options module must be created once and shared between the exports and app modules — a file may belong to only one zig module. - Desktop mirrors it in the generated runner's
else nullfallback.
Happy to reshape to whatever fits (manifest-declared host module seems most in keeping with app.json/app.zon being the single source of app truth).
Activity
0No activity yet.