Skip to content

Add JSRemote<T> for owner-thread JSObject access #704

@kateinoigakukun

Description

@kateinoigakukun

Motivation

JSObject is intentionally thread-bound in JavaScriptKit. In multi-threaded WebAssembly builds, each worker owns its own JavaScript object space, and JSObject enforces that ownership at runtime via ownerTid.

This is the right default for safety, but it leaves a gap for cases where Swift code wants to keep a stable reference to a JavaScript object while interacting with it from another isolation domain. Today the only public cross-thread escape hatch is JSSending, which transfers or clones the object into the destination thread. That is useful when ownership should move, but it does not help when the caller wants to keep using the original object on its original owner thread.

We need a small, explicit abstraction for that use case: a sendable handle that can be passed across threads, while only exposing the underlying JSObject through an async hop back to the owner thread.

Overview

This proposal introduces JSRemote<T> in JavaScriptEventLoop.

The initial scope is intentionally narrow:

  • The first version is effectively JSRemote<JSObject>.
  • The handle is Sendable.
  • It retains the original owner-side JSObject.
  • It stores the owning thread ID.
  • The wrapped object is only accessible through an async API that executes a closure on the owner thread.

The key design constraints are:

  • JSObject itself should remain non-Sendable
  • the public API should make cross-thread access explicit
  • the wrapped object should not be cloned or transferred just to perform a temporary access
  • the implementation should reuse the existing ITC request-routing mechanism already used by JSSending

API Design (draft)

The first public shape would be:

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct JSRemote<T>: @unchecked Sendable

@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension JSRemote where T == JSObject {
    public init(_ object: JSObject)

    public func withJSObject<R: Sendable>(
        _ body: (JSObject) -> R
    ) async -> R
}

Usage:

let remote = JSRemote(document)

let title = await remote.withJSObject { document in
    document.title.string ?? ""
}

Semantics:

  • withJSObject always executes body on the JSObject owner thread
  • the method is async because it may need to hop threads
  • the closure itself is synchronous and cannot suspend while borrowing the object
  • the return type is constrained to Sendable, so raw JS references do not escape back across threads

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