#430

ts-core: no official way to route Cmd.request into native code — patch-only on desktop, impossible on mobile

Opendonparkopened 1mo ago

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 possible

    Real apps doing native effects work around this by patching the generated runner in zig-cache after the build stages it (add an @import of a host module, wire its binding() in). Undocumented and fragile, but possible.

  • Mobile: the staged mobile wiring (src/app_runner/ts_core_mobile.zig) sets host_calls from 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_service are 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 exposing binding() with the same contract as the desktop runner's host_calls value, 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 null fallback.

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

0

No activity yet.