Skip to content

Commit 57a6fa2

Browse files
Merge pull request #409 from swiftwasm/yt/bjs-fix-swift-class
BridgeJS: Change `@JS init` to generate `static init()` methods instead of `constructor` in JS
2 parents cc50be3 + ed482fe commit 57a6fa2

File tree

13 files changed

+308
-25
lines changed

13 files changed

+308
-25
lines changed

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ Tests for `PackageToJS` plugin:
7171
swift test --package-path ./Plugins/PackageToJS
7272
```
7373

74+
Tests for `BridgeJS` plugin:
75+
76+
```bash
77+
swift test --package-path ./Plugins/BridgeJS
78+
```
79+
80+
To update snapshot test files when expected output changes:
81+
82+
```bash
83+
UPDATE_SNAPSHOTS=1 swift test --package-path ./Plugins/BridgeJS
84+
```
85+
7486
### Editing `./Runtime` directory
7587

7688
The `./Runtime` directory contains the JavaScript runtime that interacts with the JavaScript environment and Swift code.

Examples/ExportSwift/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
22
const { exports } = await init({});
33

44
const Greeter = exports.Greeter;
5-
const greeter = new Greeter("World");
5+
const greeter = Greeter.init("World");
66
const circle = exports.renderCircleSVG(100);
77

88
// Display the results

Examples/PlayBridgeJS/Sources/JavaScript/app.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class BridgeJSPlayground {
5252
createTS2Skeleton: this.createTS2Skeleton
5353
}
5454
});
55-
this.playBridgeJS = new exports.PlayBridgeJS();
55+
this.playBridgeJS = exports.PlayBridgeJS.init();
5656
console.log('BridgeJS initialized successfully');
5757
} catch (error) {
5858
console.error('Failed to initialize BridgeJS:', error);
@@ -162,4 +162,4 @@ export class BridgeJSPlayground {
162162
hideError() {
163163
this.errorDisplay.classList.remove('show');
164164
}
165-
}
165+
}

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ struct BridgeJSLink {
4343
let swiftHeapObjectClassJs = """
4444
/// Represents a Swift heap object like a class instance or an actor instance.
4545
class SwiftHeapObject {
46+
static __construct(ptr, deinit) {
47+
return new SwiftHeapObject(ptr, deinit);
48+
}
49+
4650
constructor(pointer, deinit) {
4751
this.pointer = pointer;
4852
this.hasReleased = false;
@@ -401,7 +405,7 @@ struct BridgeJSLink {
401405
bodyLines.append("swift.memory.release(retId);")
402406
returnExpr = "ret"
403407
case .swiftHeapObject(let name):
404-
bodyLines.append("const ret = new \(name)(\(call));")
408+
bodyLines.append("const ret = \(name).__construct(\(call));")
405409
returnExpr = "ret"
406410
}
407411
return returnExpr
@@ -484,23 +488,37 @@ struct BridgeJSLink {
484488
dtsExportEntryLines.append("\(klass.name): {")
485489
jsLines.append("class \(klass.name) extends SwiftHeapObject {")
486490

491+
// Always add __construct and constructor methods for all classes
492+
var constructorLines: [String] = []
493+
constructorLines.append("static __construct(ptr) {")
494+
constructorLines.append(
495+
"return new \(klass.name)(ptr, instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)
496+
)
497+
constructorLines.append("}")
498+
constructorLines.append("")
499+
constructorLines.append("constructor(pointer, deinit) {")
500+
constructorLines.append("super(pointer, deinit);".indent(count: 4))
501+
constructorLines.append("}")
502+
jsLines.append(contentsOf: constructorLines.map { $0.indent(count: 4) })
503+
487504
if let constructor: ExportedConstructor = klass.constructor {
488505
let thunkBuilder = ExportedThunkBuilder(effects: constructor.effects)
489506
for param in constructor.parameters {
490507
thunkBuilder.lowerParameter(param: param)
491508
}
492509
var funcLines: [String] = []
493-
funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {")
510+
funcLines.append("")
511+
funcLines.append("static init(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {")
494512
let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName)
495513
funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) })
496514
funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) })
497515
funcLines.append(contentsOf: thunkBuilder.checkExceptionLines().map { $0.indent(count: 4) })
498-
funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4))
516+
funcLines.append("return \(klass.name).__construct(\(returnExpr));".indent(count: 4))
499517
funcLines.append("}")
500518
jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) })
501519

502520
dtsExportEntryLines.append(
503-
"new\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
521+
"init\(renderTSSignature(parameters: constructor.parameters, returnType: .swiftHeapObject(klass.name)));"
504522
.indent(count: 4)
505523
)
506524
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ export interface UUID extends SwiftHeapObject {
5151
}
5252
export type Exports = {
5353
Greeter: {
54-
new(name: string): Greeter;
54+
init(name: string): Greeter;
5555
}
5656
Converter: {
57-
new(): Converter;
57+
init(): Converter;
5858
}
5959
UUID: {
6060
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Namespaces.Export.js

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export async function createInstantiator(options, swift) {
6060
const js = swift.memory.heap;
6161
/// Represents a Swift heap object like a class instance or an actor instance.
6262
class SwiftHeapObject {
63+
static __construct(ptr, deinit) {
64+
return new SwiftHeapObject(ptr, deinit);
65+
}
66+
6367
constructor(pointer, deinit) {
6468
this.pointer = pointer;
6569
this.hasReleased = false;
@@ -76,12 +80,20 @@ export async function createInstantiator(options, swift) {
7680
}
7781
}
7882
class Greeter extends SwiftHeapObject {
79-
constructor(name) {
83+
static __construct(ptr) {
84+
return new Greeter(ptr, instance.exports.bjs_Greeter_deinit);
85+
}
86+
87+
constructor(pointer, deinit) {
88+
super(pointer, deinit);
89+
}
90+
91+
static init(name) {
8092
const nameBytes = textEncoder.encode(name);
8193
const nameId = swift.memory.retain(nameBytes);
8294
const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length);
8395
swift.memory.release(nameId);
84-
super(ret, instance.exports.bjs_Greeter_deinit);
96+
return Greeter.__construct(ret);
8597
}
8698
greet() {
8799
instance.exports.bjs_Greeter_greet(this.pointer);
@@ -91,9 +103,17 @@ export async function createInstantiator(options, swift) {
91103
}
92104
}
93105
class Converter extends SwiftHeapObject {
94-
constructor() {
106+
static __construct(ptr) {
107+
return new Converter(ptr, instance.exports.bjs_Converter_deinit);
108+
}
109+
110+
constructor(pointer, deinit) {
111+
super(pointer, deinit);
112+
}
113+
114+
static init() {
95115
const ret = instance.exports.bjs_Converter_init();
96-
super(ret, instance.exports.bjs_Converter_deinit);
116+
return Converter.__construct(ret);
97117
}
98118
toString(value) {
99119
instance.exports.bjs_Converter_toString(this.pointer, value);
@@ -103,6 +123,13 @@ export async function createInstantiator(options, swift) {
103123
}
104124
}
105125
class UUID extends SwiftHeapObject {
126+
static __construct(ptr) {
127+
return new UUID(ptr, instance.exports.bjs_UUID_deinit);
128+
}
129+
130+
constructor(pointer, deinit) {
131+
super(pointer, deinit);
132+
}
106133
uuidString() {
107134
instance.exports.bjs_UUID_uuidString(this.pointer);
108135
const ret = tmpRetString;

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface Greeter extends SwiftHeapObject {
1717
}
1818
export type Exports = {
1919
Greeter: {
20-
new(name: string): Greeter;
20+
init(name: string): Greeter;
2121
}
2222
takeGreeter(greeter: Greeter): void;
2323
}

Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export async function createInstantiator(options, swift) {
6060
const js = swift.memory.heap;
6161
/// Represents a Swift heap object like a class instance or an actor instance.
6262
class SwiftHeapObject {
63+
static __construct(ptr, deinit) {
64+
return new SwiftHeapObject(ptr, deinit);
65+
}
66+
6367
constructor(pointer, deinit) {
6468
this.pointer = pointer;
6569
this.hasReleased = false;
@@ -76,12 +80,20 @@ export async function createInstantiator(options, swift) {
7680
}
7781
}
7882
class Greeter extends SwiftHeapObject {
79-
constructor(name) {
83+
static __construct(ptr) {
84+
return new Greeter(ptr, instance.exports.bjs_Greeter_deinit);
85+
}
86+
87+
constructor(pointer, deinit) {
88+
super(pointer, deinit);
89+
}
90+
91+
static init(name) {
8092
const nameBytes = textEncoder.encode(name);
8193
const nameId = swift.memory.retain(nameBytes);
8294
const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length);
8395
swift.memory.release(nameId);
84-
super(ret, instance.exports.bjs_Greeter_deinit);
96+
return Greeter.__construct(ret);
8597
}
8698
greet() {
8799
instance.exports.bjs_Greeter_greet(this.pointer);

Sources/JavaScriptKit/Documentation.docc/Articles/Exporting-Swift-to-JavaScript.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ In JavaScript:
133133
import { init } from "./.build/plugins/PackageToJS/outputs/Package/index.js";
134134
const { exports } = await init({});
135135

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

159159
export type Exports = {
160160
ShoppingCart: {
161-
new(): ShoppingCart;
161+
init(): ShoppingCart;
162162
}
163163
}
164164
```
@@ -175,8 +175,8 @@ You can export functions to specific namespaces by providing a namespace paramet
175175
import JavaScriptKit
176176

177177
// Export a function to a custom namespace
178-
@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String {
179-
return "namespaced"
178+
@JS(namespace: "MyModule.Utils") func namespacedFunction() -> String {
179+
return "namespaced"
180180
}
181181
```
182182

Tests/BridgeJSRuntimeTests/ExportAPITests.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,47 @@ struct TestError: Error {
7474
g.changeName(name: name)
7575
}
7676

77+
// Test class without @JS init constructor
78+
@JS class Calculator {
79+
nonisolated(unsafe) static var onDeinit: () -> Void = {}
80+
81+
@JS func square(value: Int) -> Int {
82+
return value * value
83+
}
84+
85+
@JS func add(a: Int, b: Int) -> Int {
86+
return a + b
87+
}
88+
89+
deinit {
90+
Self.onDeinit()
91+
}
92+
}
93+
94+
@JS func createCalculator() -> Calculator {
95+
return Calculator()
96+
}
97+
98+
@JS func useCalculator(calc: Calculator, x: Int, y: Int) -> Int {
99+
return calc.add(a: calc.square(value: x), b: y)
100+
}
101+
77102
class ExportAPITests: XCTestCase {
78103
func testAll() {
79104
var hasDeinitGreeter = false
105+
var hasDeinitCalculator = false
106+
80107
Greeter.onDeinit = {
81108
hasDeinitGreeter = true
82109
}
110+
111+
Calculator.onDeinit = {
112+
hasDeinitCalculator = true
113+
}
114+
83115
runJsWorks()
84-
XCTAssertTrue(hasDeinitGreeter)
116+
117+
XCTAssertTrue(hasDeinitGreeter, "Greeter (with @JS init) should have been deinitialized")
118+
XCTAssertTrue(hasDeinitCalculator, "Calculator (without @JS init) should have been deinitialized")
85119
}
86120
}

0 commit comments

Comments
 (0)