Skip to content

Conversation

@krodak
Copy link
Member

@krodak krodak commented Nov 5, 2025

Overview

This PR implements typed closure support in BridgeJS, enabling Swift functions to accept closures as parameters and return closures as values with full type safety.

Example

Swift:

@JS class TextProcessor {
    @JS init(transform: @escaping (String) -> String)
    @JS func processWithPerson(_ person: Person, formatter: (Person) -> String) -> String
    @JS func makePersonCreator(defaultName: String) -> (String) -> Person
}

JavaScript:

const processor = new exports.TextProcessor((text) => text.toUpperCase());

const person = new exports.Person("Alice");
const result = processor.processWithPerson(person, (p) => `${p.name}: ${p.greet()}`);

const creator = processor.makePersonCreator("Default");
const p1 = creator("Bob");

TypeScript:

export interface TextProcessor extends SwiftHeapObject {
    processWithPerson(person: Person, formatter: (arg0: Person) => string): string;
    makePersonCreator(defaultName: string): (arg0: string) => Person;
}

Technical Changes

  • Extended type system to support BridgeType.closure(ClosureSignature)
  • Updated Swift parser to detect closure parameters in function declarations
  • Generate Swift helper enums (_BJS_Closure_*) for each unique closure signature
  • Generate JavaScript callback registry and invoke functions, needed to expose _exports and _bjs to support that
  • Map closure signatures to TypeScript function types
  • Automatic memory management in both directions, needed to add bridgeJSLowerParameterWithRetain to support optional SwiftHeapObject

Supported Features

  • ✅ Closure parameters with any supported type
  • ✅ Closure return values
  • @escaping closures
  • ✅ Optional closures and optional parameters/returns
  • ✅ All existing BridgeJS types (primitives, String, enums, Swift classes)
  • ❌ Closure-typed properties (parameters/returns only)
  • ❌ Async/throwing closures

Testing

Added comprehensive snapshot and runtime tests for closure parameters, returns, optionals, and Swift heap objects in closures.

Documentation

Extended current documentation with new Exporting-Swift-Closure.md

@krodak krodak self-assigned this Nov 5, 2025
@krodak krodak added this to WasmKit Nov 5, 2025
@krodak krodak force-pushed the feat/closure-support branch from bbfaa31 to 3d17fda Compare November 5, 2025 14:05
@krodak krodak removed this from WasmKit Nov 5, 2025
}

for signature in closureSignatures.sorted(by: { $0.mangleName < $1.mangleName }) {
let invokeFuncName = "invoke_js_callback_\(signature.mangleName.lowercased())"
Copy link
Member

Choose a reason for hiding this comment

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

To be conservative, it might be better not to force lowercase here to avoid conflicts with names having different casing

? "Void"
: parameters.map { $0.mangleTypeName }.joined(separator: "_")
let returnPart = returnType.mangleTypeName
self.mangleName = "\(paramPart)_To_\(returnPart)"
Copy link
Member

Choose a reason for hiding this comment

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

Can we encode the module name into the mangled name to avoid conflicts with the same name types from other modules?

case .swiftHeapObject(let name):
return name
case .optional(let wrapped):
return "Optional\(wrapped.mangleTypeName)"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return "Optional\(wrapped.mangleTypeName)"
return "Optional<\(wrapped.mangleTypeName)>"

Let's include angle bracket not to conflict with user defined OptionalFoo nominal types

Copy link
Member Author

@krodak krodak Nov 6, 2025

Choose a reason for hiding this comment

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

This is used for functions and classes name, so we need to avoid < > as this would be treat as generics - maybe we should settle for something like: return "Optional_\(wrapped.mangleTypeName)"

That would also align with

case .closure(let signature):
            return "Closure_\(signature.mangleName)"

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I think it still has ambiguousity. How about using Swift's mangling rule here? It's very simple if we just want to mangle it. (demangling might be tricky but)
https://github.com/swiftlang/swift/blob/main/docs/ABI/Mangling.rst#types


if !closureSignatures.isEmpty {
printer.nextLine()
printer.write("bjs[\"release_js_callback\"] = function(id) {")
Copy link
Member

Choose a reason for hiding this comment

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

I think we already have swift_js_release, which does exactly the same thing

Comment on lines +57 to +72
@_spi(BridgeJS) public final class _JSCallbackOwner {
public let callbackId: Int32
private var isReleased: Bool = false

public init(callbackId: Int32) {
self.callbackId = callbackId
}

deinit {
guard !isReleased else { return }
#if arch(wasm32)
_swift_js_release_callback(callbackId)
#endif
isReleased = true
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can we consider replacing this class with JSObject? JS object handle deallocation is a bit tricky (e.g. we need to care about deallocation from other threads than it was created.), so I'd like to centralize the lifetime management in JSObject.

@krodak krodak closed this Nov 6, 2025
@krodak krodak reopened this Nov 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants