Skip to content

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

Description

@donpark

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions