Skip to content

Conversation

@kateinoigakukun
Copy link
Member

@kateinoigakukun kateinoigakukun commented Feb 4, 2026

BridgeJS: Add JSTypedClosure API

The new API allows managing JS closures converted from Swift closures from Swift side. It allows us to get the underlying JSObject (#540) and manage its lifetime manually from Swift.

@JS func makeIntToInt() throws(JSException) -> JSTypedClosure<(Int) -> Int> {
    return JSTypedClosure { x in
        return x + 1
    }
}

@JSFunction func takeIntToInt(_ transform: JSTypedClosure<(Int) -> Int>) throws(JSException)

let closure = JSTypedClosure<(Int) -> Int> { x in
    return x * 2
}
defer { closure.release() }
try takeIntToInt(closure)

Likewise to JSClosure, API users are responsible for "manually" releasing the closure when it's no longer needed by calling release(). After releasing, the closure becomes unusable and calling it will throw a JS exception (note that we will not segfault even if the closure is called after releasing).

let closure = JSTypedClosure<(Int) -> Int> { x in
    return x * 2
}
closure.release()
try closure(10)  // "JSException: Attempted to call a released JSTypedClosure created at <file>:<line>"

As a belt-and-suspenders measure, the underlying JS closure is also registered with a FinalizationRegistry to ensure that the Swift closure box is released when the JS closure is garbage collected, in case the API user
forgets to call release(). However, relying on this mechanism is not recommended because the timing of garbage collection is non-deterministic and it's not guaranteed that it will happen in a timely manner.


On the top of the new API, this commit also fixes memory leak issues of
closures exported to JS.

Close #536 #540 #541


Future TODO

  • Ban usage of JSTypedClosure in parameters of exported Swift functions and results of imported JS functions.
  • Allow closure signature to have throws(JSException)

@kateinoigakukun kateinoigakukun force-pushed the katei/7600-bridgejs-add-jst branch 3 times, most recently from 3f1660a to 4dffde8 Compare February 5, 2026 10:10
@kateinoigakukun kateinoigakukun added this to the BridgeJS MVP milestone Feb 5, 2026
@kateinoigakukun kateinoigakukun force-pushed the katei/7600-bridgejs-add-jst branch 9 times, most recently from cc2776a to ccd8250 Compare February 6, 2026 04:54
The new API allows managing JS closures converted from Swift closures
from Swift side. It allows us to get the underlying `JSObject` and
manage its lifetime manually from Swift.

```swift
@js func makeIntToInt() throws(JSException) -> JSTypedClosure<(Int) -> Int> {
    return JSTypedClosure { x in
        return x + 1
    }
}

@JSFunction func takeIntToInt(_ transform: JSTypedClosure<(Int) -> Int>) throws(JSException)

let closure = JSTypedClosure<(Int) -> Int> { x in
    return x * 2
}
defer { closure.release() }
try takeIntToInt(closure)
```

Likewise to `JSClosure`, API users are responsible for "manually" releasing the
closure when it's no longer needed by calling `release()`. After releasing,
the closure becomes unusable and calling it will throw a JS exception
(note that we will not segfault even if the closure is called after releasing).

```swift
let closure = JSTypedClosure<(Int) -> Int> { x in
    return x * 2
}
closure.release()
try closure(10)  // "JSException: Attempted to call a released JSTypedClosure created at <file>:<line>"
```

As a belt-and-suspenders measure, the underlying JS closure is also
registered with a `FinalizationRegistry` to ensure that the Swift closure box
is released when the JS closure is garbage collected, in case the API user
forgets to call `release()`. However, relying on this mechanism is **not recommended**
because the timing of garbage collection is non-deterministic and it's
not guaranteed that it will happen in a timely manner.

----

On the top of the new API, this commit also fixes memory leak issues of
closures exported to JS.
Copy link
Member

@krodak krodak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌🏻 🙇🏻

switch returnType {
case .closure(let signature):
append("return _BJS_Closure_\(raw: signature.mangleName).bridgeJSLower(ret)")
case .closure(_, useJSTypedClosure: false):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: For useJSTypedClosure: true we intentionally fall-through to default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm intentionally trying to use the default case (ret.bridgeJSLowerReturn()) as much as possible to simplify things. JSTypedClosure has bridgeJSLowerReturn definition for this purpose.

// let obj3 = JSObject()
// let result = try ClosureSupportImports.jsApplyArrayJSObject([obj1, obj2, obj3], transform)
// XCTAssertEqual(result, [obj1, obj2, obj3])
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Do we need some TODO or linked issue for those commented ones?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes for sure #598

@kateinoigakukun kateinoigakukun merged commit 8217d06 into main Feb 6, 2026
11 checks passed
@kateinoigakukun kateinoigakukun deleted the katei/7600-bridgejs-add-jst branch February 6, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants