Skip to content

BridgeJS: Change @JS init to generate static init() methods instead of constructor in JS #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ Tests for `PackageToJS` plugin:
swift test --package-path ./Plugins/PackageToJS
```

Tests for `BridgeJS` plugin:

```bash
swift test --package-path ./Plugins/BridgeJS
```

To update snapshot test files when expected output changes:

```bash
UPDATE_SNAPSHOTS=1 swift test --package-path ./Plugins/BridgeJS
```

### Editing `./Runtime` directory

The `./Runtime` directory contains the JavaScript runtime that interacts with the JavaScript environment and Swift code.
Expand Down
2 changes: 1 addition & 1 deletion Examples/ExportSwift/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
const { exports } = await init({});

const Greeter = exports.Greeter;
const greeter = new Greeter("World");
const greeter = Greeter.init("World");
const circle = exports.renderCircleSVG(100);

// Display the results
Expand Down
4 changes: 2 additions & 2 deletions Examples/PlayBridgeJS/Sources/JavaScript/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class BridgeJSPlayground {
createTS2Skeleton: this.createTS2Skeleton
}
});
this.playBridgeJS = new exports.PlayBridgeJS();
this.playBridgeJS = exports.PlayBridgeJS.init();
console.log('BridgeJS initialized successfully');
} catch (error) {
console.error('Failed to initialize BridgeJS:', error);
Expand Down Expand Up @@ -162,4 +162,4 @@ export class BridgeJSPlayground {
hideError() {
this.errorDisplay.classList.remove('show');
}
}
}
26 changes: 22 additions & 4 deletions Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ struct BridgeJSLink {
let swiftHeapObjectClassJs = """
/// Represents a Swift heap object like a class instance or an actor instance.
class SwiftHeapObject {
static __construct(ptr, deinit) {
return new SwiftHeapObject(ptr, deinit);
}

constructor(pointer, deinit) {
this.pointer = pointer;
this.hasReleased = false;
Expand Down Expand Up @@ -401,7 +405,7 @@ struct BridgeJSLink {
bodyLines.append("swift.memory.release(retId);")
returnExpr = "ret"
case .swiftHeapObject(let name):
bodyLines.append("const ret = new \(name)(\(call));")
bodyLines.append("const ret = \(name).__construct(\(call));")
returnExpr = "ret"
}
return returnExpr
Expand Down Expand Up @@ -484,23 +488,37 @@ struct BridgeJSLink {
dtsExportEntryLines.append("\(klass.name): {")
jsLines.append("class \(klass.name) extends SwiftHeapObject {")

// Always add __construct and constructor methods for all classes
var constructorLines: [String] = []
constructorLines.append("static __construct(ptr) {")
constructorLines.append(
"return new \(klass.name)(ptr, instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)
)
constructorLines.append("}")
constructorLines.append("")
constructorLines.append("constructor(pointer, deinit) {")
constructorLines.append("super(pointer, deinit);".indent(count: 4))
constructorLines.append("}")
jsLines.append(contentsOf: constructorLines.map { $0.indent(count: 4) })

if let constructor: ExportedConstructor = klass.constructor {
let thunkBuilder = ExportedThunkBuilder(effects: constructor.effects)
for param in constructor.parameters {
thunkBuilder.lowerParameter(param: param)
}
var funcLines: [String] = []
funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {")
funcLines.append("")
funcLines.append("static init(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {")
let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName)
funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) })
funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) })
funcLines.append(contentsOf: thunkBuilder.checkExceptionLines().map { $0.indent(count: 4) })
funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4))
funcLines.append("return \(klass.name).__construct(\(returnExpr));".indent(count: 4))
funcLines.append("}")
jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) })

dtsExportEntryLines.append(
"new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
"init\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
.indent(count: 4)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ export interface UUID extends SwiftHeapObject {
}
export type Exports = {
Greeter: {
new(name: string): Greeter;
init(name: string): Greeter;
}
Converter: {
new(): Converter;
init(): Converter;
}
UUID: {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export async function createInstantiator(options, swift) {
const js = swift.memory.heap;
/// Represents a Swift heap object like a class instance or an actor instance.
class SwiftHeapObject {
static __construct(ptr, deinit) {
return new SwiftHeapObject(ptr, deinit);
}

constructor(pointer, deinit) {
this.pointer = pointer;
this.hasReleased = false;
Expand All @@ -76,12 +80,20 @@ export async function createInstantiator(options, swift) {
}
}
class Greeter extends SwiftHeapObject {
constructor(name) {
static __construct(ptr) {
return new Greeter(ptr, instance.exports.bjs_Greeter_deinit);
}

constructor(pointer, deinit) {
super(pointer, deinit);
}

static init(name) {
const nameBytes = textEncoder.encode(name);
const nameId = swift.memory.retain(nameBytes);
const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length);
swift.memory.release(nameId);
super(ret, instance.exports.bjs_Greeter_deinit);
return Greeter.__construct(ret);
}
greet() {
instance.exports.bjs_Greeter_greet(this.pointer);
Expand All @@ -91,9 +103,17 @@ export async function createInstantiator(options, swift) {
}
}
class Converter extends SwiftHeapObject {
constructor() {
static __construct(ptr) {
return new Converter(ptr, instance.exports.bjs_Converter_deinit);
}

constructor(pointer, deinit) {
super(pointer, deinit);
}

static init() {
const ret = instance.exports.bjs_Converter_init();
super(ret, instance.exports.bjs_Converter_deinit);
return Converter.__construct(ret);
}
toString(value) {
instance.exports.bjs_Converter_toString(this.pointer, value);
Expand All @@ -103,6 +123,13 @@ export async function createInstantiator(options, swift) {
}
}
class UUID extends SwiftHeapObject {
static __construct(ptr) {
return new UUID(ptr, instance.exports.bjs_UUID_deinit);
}

constructor(pointer, deinit) {
super(pointer, deinit);
}
uuidString() {
instance.exports.bjs_UUID_uuidString(this.pointer);
const ret = tmpRetString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface Greeter extends SwiftHeapObject {
}
export type Exports = {
Greeter: {
new(name: string): Greeter;
init(name: string): Greeter;
}
takeGreeter(greeter: Greeter): void;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export async function createInstantiator(options, swift) {
const js = swift.memory.heap;
/// Represents a Swift heap object like a class instance or an actor instance.
class SwiftHeapObject {
static __construct(ptr, deinit) {
return new SwiftHeapObject(ptr, deinit);
}

constructor(pointer, deinit) {
this.pointer = pointer;
this.hasReleased = false;
Expand All @@ -76,12 +80,20 @@ export async function createInstantiator(options, swift) {
}
}
class Greeter extends SwiftHeapObject {
constructor(name) {
static __construct(ptr) {
return new Greeter(ptr, instance.exports.bjs_Greeter_deinit);
}

constructor(pointer, deinit) {
super(pointer, deinit);
}

static init(name) {
const nameBytes = textEncoder.encode(name);
const nameId = swift.memory.retain(nameBytes);
const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length);
swift.memory.release(nameId);
super(ret, instance.exports.bjs_Greeter_deinit);
return Greeter.__construct(ret);
}
greet() {
instance.exports.bjs_Greeter_greet(this.pointer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ In JavaScript:
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
const { exports } = await init({});

const cart = new exports.ShoppingCart();
const cart = exports.ShoppingCart.init();
cart.addItem("Laptop", 999.99, 1);
cart.addItem("Mouse", 24.99, 2);
console.log(`Items in cart: ${cart.getItemCount()}`);
Expand All @@ -158,7 +158,7 @@ export interface ShoppingCart extends SwiftHeapObject {

export type Exports = {
ShoppingCart: {
new(): ShoppingCart;
init(): ShoppingCart;
}
}
```
Expand All @@ -175,8 +175,8 @@ You can export functions to specific namespaces by providing a namespace paramet
import JavaScriptKit

// Export a function to a custom namespace
@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String {
return "namespaced"
@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String {
return "namespaced"
}
```

Expand Down
36 changes: 35 additions & 1 deletion Tests/BridgeJSRuntimeTests/ExportAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,47 @@ struct TestError: Error {
g.changeName(name: name)
}

// Test class without @JS init constructor
@JS class Calculator {
nonisolated(unsafe) static var onDeinit: () -> Void = {}

@JS func square(value: Int) -> Int {
return value * value
}

@JS func add(a: Int, b: Int) -> Int {
return a + b
}

deinit {
Self.onDeinit()
}
}

@JS func createCalculator() -> Calculator {
return Calculator()
}

@JS func useCalculator(calc: Calculator, x: Int, y: Int) -> Int {
return calc.add(a: calc.square(value: x), b: y)
}

class ExportAPITests: XCTestCase {
func testAll() {
var hasDeinitGreeter = false
var hasDeinitCalculator = false

Greeter.onDeinit = {
hasDeinitGreeter = true
}

Calculator.onDeinit = {
hasDeinitCalculator = true
}

runJsWorks()
XCTAssertTrue(hasDeinitGreeter)

XCTAssertTrue(hasDeinitGreeter, "Greeter (with @JS init) should have been deinitialized")
XCTAssertTrue(hasDeinitCalculator, "Calculator (without @JS init) should have been deinitialized")
}
}
50 changes: 50 additions & 0 deletions Tests/BridgeJSRuntimeTests/Generated/BridgeJS.ExportSwift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,28 @@ public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameL
#endif
}

@_expose(wasm, "bjs_createCalculator")
@_cdecl("bjs_createCalculator")
public func _bjs_createCalculator() -> UnsafeMutableRawPointer {
#if arch(wasm32)
let ret = createCalculator()
return Unmanaged.passRetained(ret).toOpaque()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_useCalculator")
@_cdecl("bjs_useCalculator")
public func _bjs_useCalculator(calc: UnsafeMutableRawPointer, x: Int32, y: Int32) -> Int32 {
#if arch(wasm32)
let ret = useCalculator(calc: Unmanaged<Calculator>.fromOpaque(calc).takeUnretainedValue(), x: Int(x), y: Int(y))
return Int32(ret)
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_Greeter_init")
@_cdecl("bjs_Greeter_init")
public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer {
Expand Down Expand Up @@ -360,4 +382,32 @@ public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: I
@_cdecl("bjs_Greeter_deinit")
public func _bjs_Greeter_deinit(pointer: UnsafeMutableRawPointer) {
Unmanaged<Greeter>.fromOpaque(pointer).release()
}

@_expose(wasm, "bjs_Calculator_square")
@_cdecl("bjs_Calculator_square")
public func _bjs_Calculator_square(_self: UnsafeMutableRawPointer, value: Int32) -> Int32 {
#if arch(wasm32)
let ret = Unmanaged<Calculator>.fromOpaque(_self).takeUnretainedValue().square(value: Int(value))
return Int32(ret)
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_Calculator_add")
@_cdecl("bjs_Calculator_add")
public func _bjs_Calculator_add(_self: UnsafeMutableRawPointer, a: Int32, b: Int32) -> Int32 {
#if arch(wasm32)
let ret = Unmanaged<Calculator>.fromOpaque(_self).takeUnretainedValue().add(a: Int(a), b: Int(b))
return Int32(ret)
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_Calculator_deinit")
@_cdecl("bjs_Calculator_deinit")
public func _bjs_Calculator_deinit(pointer: UnsafeMutableRawPointer) {
Unmanaged<Calculator>.fromOpaque(pointer).release()
}
Loading