diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index e62a9a63..0680a3d3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -47,10 +47,8 @@ struct BridgeJSLink { func link() throws -> (outputJs: String, outputDts: String) { var exportsLines: [String] = [] - var importedLines: [String] = [] var classLines: [String] = [] var dtsExportLines: [String] = [] - var dtsImportLines: [String] = [] var dtsClassLines: [String] = [] if exportedSkeletons.contains(where: { $0.classes.count > 0 }) { @@ -84,57 +82,18 @@ struct BridgeJSLink { } } + var importObjectBuilders: [ImportObjectBuilder] = [] for skeletonSet in importedSkeletons { - importedLines.append("const \(skeletonSet.moduleName) = importObject[\"\(skeletonSet.moduleName)\"] = {};") - func assignToImportObject(name: String, function: [String]) { - var js = function - js[0] = "\(skeletonSet.moduleName)[\"\(name)\"] = " + js[0] - importedLines.append(contentsOf: js) - } + let importObjectBuilder = ImportObjectBuilder(moduleName: skeletonSet.moduleName) for fileSkeleton in skeletonSet.children { for function in fileSkeleton.functions { - let (js, dts) = try renderImportedFunction(function: function) - assignToImportObject(name: function.abiName(context: nil), function: js) - dtsImportLines.append(contentsOf: dts) + try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function) } for type in fileSkeleton.types { - for property in type.properties { - let getterAbiName = property.getterAbiName(context: type) - let (js, dts) = try renderImportedProperty( - property: property, - abiName: getterAbiName, - emitCall: { thunkBuilder in - thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type) - return try thunkBuilder.lowerReturnValue(returnType: property.type) - } - ) - assignToImportObject(name: getterAbiName, function: js) - dtsImportLines.append(contentsOf: dts) - - if !property.isReadonly { - let setterAbiName = property.setterAbiName(context: type) - let (js, dts) = try renderImportedProperty( - property: property, - abiName: setterAbiName, - emitCall: { thunkBuilder in - thunkBuilder.liftParameter( - param: Parameter(label: nil, name: "newValue", type: property.type) - ) - thunkBuilder.callPropertySetter(name: property.name, returnType: property.type) - return nil - } - ) - assignToImportObject(name: setterAbiName, function: js) - dtsImportLines.append(contentsOf: dts) - } - } - for method in type.methods { - let (js, dts) = try renderImportedMethod(context: type, method: method) - assignToImportObject(name: method.abiName(context: type), function: js) - dtsImportLines.append(contentsOf: dts) - } + try renderImportedType(importObjectBuilder: importObjectBuilder, type: type) } } + importObjectBuilders.append(importObjectBuilder) } let outputJs = """ @@ -175,7 +134,7 @@ struct BridgeJSLink { target.set(tmpRetBytes); tmpRetBytes = undefined; } - \(importedLines.map { $0.indent(count: 12) }.joined(separator: "\n")) + \(importObjectBuilders.flatMap { $0.importedLines }.map { $0.indent(count: 12) }.joined(separator: "\n")) }, setInstance: (i) => { instance = i; @@ -198,7 +157,7 @@ struct BridgeJSLink { dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) }) dtsLines.append("}") dtsLines.append("export type Imports = {") - dtsLines.append(contentsOf: dtsImportLines.map { $0.indent(count: 4) }) + dtsLines.append(contentsOf: importObjectBuilders.flatMap { $0.dtsImportLines }.map { $0.indent(count: 4) }) dtsLines.append("}") let outputDts = """ // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, @@ -437,6 +396,11 @@ struct BridgeJSLink { } } + func callConstructor(name: String) { + let call = "new options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))" + bodyLines.append("let ret = \(call);") + } + func callMethod(name: String, returnType: BridgeType) { let call = "swift.memory.getObject(self).\(name)(\(parameterForwardings.joined(separator: ", ")))" if returnType == .void { @@ -475,7 +439,31 @@ struct BridgeJSLink { } } - func renderImportedFunction(function: ImportedFunctionSkeleton) throws -> (js: [String], dts: [String]) { + class ImportObjectBuilder { + var moduleName: String + var importedLines: [String] = [] + var dtsImportLines: [String] = [] + + init(moduleName: String) { + self.moduleName = moduleName + importedLines.append("const \(moduleName) = importObject[\"\(moduleName)\"] = {};") + } + + func assignToImportObject(name: String, function: [String]) { + var js = function + js[0] = "\(moduleName)[\"\(name)\"] = " + js[0] + importedLines.append(contentsOf: js) + } + + func appendDts(_ lines: [String]) { + dtsImportLines.append(contentsOf: lines) + } + } + + func renderImportedFunction( + importObjectBuilder: ImportObjectBuilder, + function: ImportedFunctionSkeleton + ) throws { let thunkBuilder = ImportedThunkBuilder() for param in function.parameters { thunkBuilder.liftParameter(param: param) @@ -486,11 +474,85 @@ struct BridgeJSLink { name: function.abiName(context: nil), returnExpr: returnExpr ) - var dtsLines: [String] = [] - dtsLines.append( - "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));" + importObjectBuilder.appendDts( + [ + "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));" + ] ) - return (funcLines, dtsLines) + importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines) + } + + func renderImportedType( + importObjectBuilder: ImportObjectBuilder, + type: ImportedTypeSkeleton + ) throws { + if let constructor = type.constructor { + try renderImportedConstructor( + importObjectBuilder: importObjectBuilder, + type: type, + constructor: constructor + ) + } + for property in type.properties { + let getterAbiName = property.getterAbiName(context: type) + let (js, dts) = try renderImportedProperty( + property: property, + abiName: getterAbiName, + emitCall: { thunkBuilder in + thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type) + return try thunkBuilder.lowerReturnValue(returnType: property.type) + } + ) + importObjectBuilder.assignToImportObject(name: getterAbiName, function: js) + importObjectBuilder.appendDts(dts) + + if !property.isReadonly { + let setterAbiName = property.setterAbiName(context: type) + let (js, dts) = try renderImportedProperty( + property: property, + abiName: setterAbiName, + emitCall: { thunkBuilder in + thunkBuilder.liftParameter( + param: Parameter(label: nil, name: "newValue", type: property.type) + ) + thunkBuilder.callPropertySetter(name: property.name, returnType: property.type) + return nil + } + ) + importObjectBuilder.assignToImportObject(name: setterAbiName, function: js) + importObjectBuilder.appendDts(dts) + } + } + for method in type.methods { + let (js, dts) = try renderImportedMethod(context: type, method: method) + importObjectBuilder.assignToImportObject(name: method.abiName(context: type), function: js) + importObjectBuilder.appendDts(dts) + } + } + + func renderImportedConstructor( + importObjectBuilder: ImportObjectBuilder, + type: ImportedTypeSkeleton, + constructor: ImportedConstructorSkeleton + ) throws { + let thunkBuilder = ImportedThunkBuilder() + for param in constructor.parameters { + thunkBuilder.liftParameter(param: param) + } + let returnType = BridgeType.jsObject(type.name) + thunkBuilder.callConstructor(name: type.name) + let returnExpr = try thunkBuilder.lowerReturnValue(returnType: returnType) + let abiName = constructor.abiName(context: type) + let funcLines = thunkBuilder.renderFunction( + name: abiName, + returnExpr: returnExpr + ) + importObjectBuilder.assignToImportObject(name: abiName, function: funcLines) + importObjectBuilder.appendDts([ + "\(type.name): {", + "new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4), + "}", + ]) } func renderImportedProperty( @@ -552,8 +614,8 @@ extension BridgeType { return "number" case .bool: return "boolean" - case .jsObject: - return "any" + case .jsObject(let name): + return name ?? "any" case .swiftHeapObject(let name): return name } diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift index a97550bd..bf269a95 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift @@ -237,7 +237,7 @@ struct ImportTS { preconditionFailure("assignThis can only be called with a jsObject return type") } abiReturnType = .i32 - body.append("self.this = ret") + body.append("self.this = JSObject(id: UInt32(bitPattern: ret))") } func renderImportDecl() -> DeclSyntax { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts index 1e7ca6ab..ffcbcd14 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts @@ -7,7 +7,7 @@ export type Exports = { } export type Imports = { - returnAnimatable(): any; + returnAnimatable(): Animatable; } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts index 818d57a9..bcbcf06f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts @@ -7,6 +7,9 @@ export type Exports = { } export type Imports = { + Greeter: { + new(name: string): Greeter; + } } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index c7ae6a22..2111af96 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -36,6 +36,12 @@ export async function createInstantiator(options, swift) { tmpRetBytes = undefined; } const TestModule = importObject["TestModule"] = {}; + TestModule["bjs_Greeter_init"] = function bjs_Greeter_init(name) { + const nameObject = swift.memory.getObject(name); + swift.memory.release(name); + let ret = new options.imports.Greeter(nameObject); + return swift.memory.retain(ret); + } TestModule["bjs_Greeter_greet"] = function bjs_Greeter_greet(self) { let ret = swift.memory.getObject(self).greet(); tmpRetBytes = textEncoder.encode(ret); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift index 993a1417..0f1f42d1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift @@ -34,7 +34,7 @@ struct Greeter { _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) } let ret = bjs_Greeter_init(nameId) - self.this = ret + self.this = JSObject(id: UInt32(bitPattern: ret)) } func greet() -> String { diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index 34031628..e7444e90 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -65,7 +65,7 @@ const harnesses = { if (preludeScript) { const prelude = await import(preludeScript) if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: true }) + options = await prelude.setupOptions(options, { isMainThread: true }) } } process.on("beforeExit", () => { diff --git a/Plugins/PackageToJS/Templates/platforms/node.js b/Plugins/PackageToJS/Templates/platforms/node.js index c45bdf35..aff708be 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.js +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -59,7 +59,7 @@ export function createDefaultWorkerFactory(preludeScript) { if (preludeScript) { const prelude = await import(preludeScript); if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: false }) + options = await prelude.setupOptions(options, { isMainThread: false }) } } await instantiateForThread(tid, startArg, { diff --git a/Plugins/PackageToJS/Templates/test.d.ts b/Plugins/PackageToJS/Templates/test.d.ts index 2968f6dd..21383997 100644 --- a/Plugins/PackageToJS/Templates/test.d.ts +++ b/Plugins/PackageToJS/Templates/test.d.ts @@ -1,5 +1,12 @@ import type { InstantiateOptions, instantiate } from "./instantiate"; +export type SetupOptionsFn = ( + options: InstantiateOptions, + context: { + isMainThread: boolean, + } +) => Promise + export function testBrowser( options: { preludeScript?: string, diff --git a/Plugins/PackageToJS/Templates/test.js b/Plugins/PackageToJS/Templates/test.js index b44b0d6e..518dacf2 100644 --- a/Plugins/PackageToJS/Templates/test.js +++ b/Plugins/PackageToJS/Templates/test.js @@ -157,6 +157,7 @@ export async function testBrowserInPage(options, processInfo) { }); const { instantiate } = await import("./instantiate.js"); + /** @type {import('./test.d.ts').SetupOptionsFn} */ let setupOptions = (options, _) => { return options }; if (processInfo.preludeScript) { const prelude = await import(processInfo.preludeScript); diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift index 9ecffea5..f479a071 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -47,4 +47,48 @@ func jsRoundTripString(_ v: String) -> String { _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) return Int(ret) } +} + +struct JsGreeter { + let this: JSObject + + init(this: JSObject) { + self.this = this + } + + init(takingThis this: Int32) { + self.this = JSObject(id: UInt32(bitPattern: this)) + } + + init(_ name: String) { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init") + func bjs_JsGreeter_init(_ name: Int32) -> Int32 + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + let ret = bjs_JsGreeter_init(nameId) + self.this = JSObject(id: UInt32(bitPattern: ret)) + } + + func greet() -> String { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_greet") + func bjs_JsGreeter_greet(_ self: Int32) -> Int32 + let ret = bjs_JsGreeter_greet(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + + func changeName(_ name: String) -> Void { + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_changeName") + func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_JsGreeter_changeName(Int32(bitPattern: self.this.id), nameId) + } + } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json index 9db7f698..867957d9 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json @@ -69,7 +69,55 @@ } ], "types" : [ + { + "constructor" : { + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + } + ] + }, + "methods" : [ + { + "name" : "greet", + "parameters" : [ + ], + "returnType" : { + "string" : { + + } + } + }, + { + "name" : "changeName", + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "JsGreeter", + "properties" : [ + + ] + } ] } ], diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift index 98479d20..bc50f9f1 100644 --- a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -34,4 +34,11 @@ class ImportAPITests: XCTestCase { XCTAssertEqual(jsRoundTripString(v), v) } } + + func testClass() { + let greeter = JsGreeter("Alice") + XCTAssertEqual(greeter.greet(), "Hello, Alice!") + greeter.changeName("Bob") + XCTAssertEqual(greeter.greet(), "Hello, Bob!") + } } diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts index 1a092f90..d2a54f05 100644 --- a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts +++ b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts @@ -2,3 +2,9 @@ export function jsRoundTripVoid(): void export function jsRoundTripNumber(v: number): number export function jsRoundTripBool(v: boolean): boolean export function jsRoundTripString(v: string): string + +export class JsGreeter { + constructor(name: string); + greet(): string; + changeName(name: string): void; +} \ No newline at end of file diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index a1af2a76..24a194f9 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,5 +1,7 @@ -/** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptions} */ -export function setupOptions(options, context) { +// @ts-check + +/** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ +export async function setupOptions(options, context) { Error.stackTraceLimit = 100; setupTestGlobals(globalThis); return { @@ -17,6 +19,19 @@ export function setupOptions(options, context) { "jsRoundTripString": (v) => { return v; }, + JsGreeter: class { + /** @param {string} name */ + constructor(name) { + this.name = name; + } + greet() { + return `Hello, ${this.name}!`; + } + /** @param {string} name */ + changeName(name) { + this.name = name; + } + } }, addToCoreImports(importObject, importsContext) { const { getInstance, getExports } = importsContext;