From cc5e8a0eaf2350bc066a6c80d081f9c9e1821077 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Mon, 9 Feb 2026 08:09:26 +0900 Subject: [PATCH] BridgeJS: Support `@JS class` in imported function signatures --- .../Sources/BridgeJSCore/ImportTS.swift | 28 ++----------- .../Sources/BridgeJSLink/JSGlueGen.swift | 22 ++--------- .../Inputs/MacroSwift/SwiftClass.swift | 2 + .../BridgeJSCodegenTests/SwiftClass.json | 29 ++++++++++++++ .../BridgeJSCodegenTests/SwiftClass.swift | 20 +++++++++- .../BridgeJSLinkTests/Protocol.js | 2 +- .../BridgeJSLinkTests/SwiftClass.d.ts | 1 + .../BridgeJSLinkTests/SwiftClass.js | 11 ++++++ .../Generated/BridgeJS.swift | 18 +++++++++ .../Generated/JavaScript/BridgeJS.json | 39 +++++++++++++++++++ .../JavaScript/SwiftClassSupportTests.mjs | 10 +++++ .../SwiftClassSupportTests.swift | 13 +++++++ Tests/prelude.mjs | 2 + 13 files changed, 153 insertions(+), 44 deletions(-) create mode 100644 Tests/BridgeJSRuntimeTests/JavaScript/SwiftClassSupportTests.mjs create mode 100644 Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 0424d8ca0..08366abf3 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -957,18 +957,8 @@ extension BridgeType { return LoweringParameterInfo(loweredParameters: [("funcRef", .i32)]) case .unsafePointer: return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)]) - case .swiftHeapObject(let className): - switch context { - case .importTS: - throw BridgeJSCoreError( - """ - swiftHeapObject '\(className)' is not supported in TypeScript imports. - Swift classes can only be used in @JS protocols where Swift owns the instance. - """ - ) - case .exportSwift: - return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)]) - } + case .swiftHeapObject: + return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)]) case .swiftProtocol: throw BridgeJSCoreError("swiftProtocol is not supported in imported signatures") case .caseEnum: @@ -1038,18 +1028,8 @@ extension BridgeType { return LiftingReturnInfo(valueToLift: .i32) case .unsafePointer: return LiftingReturnInfo(valueToLift: .pointer) - case .swiftHeapObject(let className): - switch context { - case .importTS: - throw BridgeJSCoreError( - """ - swiftHeapObject '\(className)' cannot be returned from imported TypeScript functions. - JavaScript cannot create Swift heap objects. - """ - ) - case .exportSwift: - return LiftingReturnInfo(valueToLift: .pointer) - } + case .swiftHeapObject: + return LiftingReturnInfo(valueToLift: .pointer) case .swiftProtocol: throw BridgeJSCoreError("swiftProtocol is not supported in imported signatures") case .caseEnum: diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index dec77b3cb..68230e8d1 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -581,7 +581,7 @@ struct IntrinsicJSFragment: Sendable { return IntrinsicJSFragment( parameters: ["pointer"], printCode: { arguments, scope, printer, cleanupCode in - return ["\(name).__construct(\(arguments[0]))"] + return ["_exports['\(name)'].__construct(\(arguments[0]))"] } ) } @@ -1985,14 +1985,7 @@ struct IntrinsicJSFragment: Sendable { case .jsValue: return .jsValueLiftParameter case .unsafePointer: return .identity case .swiftHeapObject(let name): - switch context { - case .importTS: - throw BridgeJSLinkError( - message: "swiftHeapObject '\(name)' can only be used in protocol exports, not in \(context)" - ) - case .exportSwift: - return .swiftHeapObjectLiftParameter(name) - } + return .swiftHeapObjectLiftParameter(name) case .swiftProtocol: return .jsObjectLiftParameter case .void: throw BridgeJSLinkError( @@ -2074,15 +2067,8 @@ struct IntrinsicJSFragment: Sendable { case .jsObject: return .jsObjectLowerReturn case .jsValue: return .jsValueLowerReturn(context: context) case .unsafePointer: return .identity - case .swiftHeapObject(let name): - switch context { - case .importTS: - throw BridgeJSLinkError( - message: "swiftHeapObject '\(name)' can only be used in protocol exports, not in \(context)" - ) - case .exportSwift: - return .swiftHeapObjectLowerReturn - } + case .swiftHeapObject: + return .swiftHeapObjectLowerReturn case .swiftProtocol: return .jsObjectLowerReturn case .void: return .void case .nullable(let wrappedType, let kind): diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift index 116b00878..aa520783a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/SwiftClass.swift @@ -18,3 +18,5 @@ @JS public class PublicGreeter {} @JS package class PackageGreeter {} + +@JSFunction func jsRoundTripGreeter(greeter: Greeter) throws(JSException) -> Greeter diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json index 9fe717651..d1bf42cff 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.json @@ -141,5 +141,34 @@ ] }, + "imported" : { + "children" : [ + { + "functions" : [ + { + "name" : "jsRoundTripGreeter", + "parameters" : [ + { + "name" : "greeter", + "type" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + } + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + } + ], + "types" : [ + + ] + } + ] + }, "moduleName" : "TestModule" } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift index 216dfc684..581d6f1c0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/SwiftClass.swift @@ -134,4 +134,22 @@ fileprivate func _bjs_PackageGreeter_wrap(_ pointer: UnsafeMutableRawPointer) -> fileprivate func _bjs_PackageGreeter_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { fatalError("Only available on WebAssembly") } -#endif \ No newline at end of file +#endif + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_jsRoundTripGreeter") +fileprivate func bjs_jsRoundTripGreeter(_ greeter: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer +#else +fileprivate func bjs_jsRoundTripGreeter(_ greeter: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripGreeter(_ greeter: Greeter) throws(JSException) -> Greeter { + let greeterPointer = greeter.bridgeJSLowerParameter() + let ret = bjs_jsRoundTripGreeter(greeterPointer) + if let error = _swift_js_take_exception() { + throw error + } + return Greeter.bridgeJSLiftReturn(ret) +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js index 57587d84a..00b9626e3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Protocol.js @@ -513,7 +513,7 @@ export async function createInstantiator(options, swift) { } TestModule["bjs_MyViewControllerDelegate_onHelperUpdated"] = function bjs_MyViewControllerDelegate_onHelperUpdated(self, helper) { try { - swift.memory.getObject(self).onHelperUpdated(Helper.__construct(helper)); + swift.memory.getObject(self).onHelperUpdated(_exports['Helper'].__construct(helper)); } catch (error) { setException(error); } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts index 8718463a9..d3461f8c1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.d.ts @@ -31,6 +31,7 @@ export type Exports = { takeGreeter(greeter: Greeter): void; } export type Imports = { + jsRoundTripGreeter(greeter: Greeter): Greeter; } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js index 6002d190f..eaa5d7720 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.js @@ -38,6 +38,7 @@ export async function createInstantiator(options, swift) { addImports: (importObject, importsContext) => { bjs = {}; importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); bjs["swift_js_return_string"] = function(ptr, len) { const bytes = new Uint8Array(memory.buffer, ptr, len); tmpRetString = textDecoder.decode(bytes); @@ -214,6 +215,16 @@ export async function createInstantiator(options, swift) { const obj = PublicGreeter.__construct(pointer); return swift.memory.retain(obj); }; + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_jsRoundTripGreeter"] = function bjs_jsRoundTripGreeter(greeter) { + try { + let ret = imports.jsRoundTripGreeter(_exports['Greeter'].__construct(greeter)); + return ret.pointer; + } catch (error) { + setException(error); + return 0 + } + } }, setInstance: (i) => { instance = i; diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 163c9e479..1feb3eed6 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -10666,4 +10666,22 @@ func _$jsRoundTripOptionalStringUndefined(_ name: JSUndefinedOr) throws( throw error } return JSUndefinedOr.bridgeJSLiftReturnFromSideChannel() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_SwiftClassSupportImports_jsRoundTripGreeter_static") +fileprivate func bjs_SwiftClassSupportImports_jsRoundTripGreeter_static(_ greeter: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer +#else +fileprivate func bjs_SwiftClassSupportImports_jsRoundTripGreeter_static(_ greeter: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + fatalError("Only available on WebAssembly") +} +#endif + +func _$SwiftClassSupportImports_jsRoundTripGreeter(_ greeter: Greeter) throws(JSException) -> Greeter { + let greeterPointer = greeter.bridgeJSLowerParameter() + let ret = bjs_SwiftClassSupportImports_jsRoundTripGreeter_static(greeterPointer) + if let error = _swift_js_take_exception() { + throw error + } + return Greeter.bridgeJSLiftReturn(ret) } \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 23f47b03d..de7ab4a5d 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -15310,6 +15310,45 @@ "types" : [ ] + }, + { + "functions" : [ + + ], + "types" : [ + { + "getters" : [ + + ], + "methods" : [ + + ], + "name" : "SwiftClassSupportImports", + "setters" : [ + + ], + "staticMethods" : [ + { + "name" : "jsRoundTripGreeter", + "parameters" : [ + { + "name" : "greeter", + "type" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + } + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + } + ] + } + ] } ] }, diff --git a/Tests/BridgeJSRuntimeTests/JavaScript/SwiftClassSupportTests.mjs b/Tests/BridgeJSRuntimeTests/JavaScript/SwiftClassSupportTests.mjs new file mode 100644 index 000000000..fdb5d498d --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/JavaScript/SwiftClassSupportTests.mjs @@ -0,0 +1,10 @@ +/** + * @returns {import('../../../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Imports["SwiftClassSupportImports"]} + */ +export function getImports(importsContext) { + return { + jsRoundTripGreeter: (greeter) => { + return greeter; + }, + }; +} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift b/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift new file mode 100644 index 000000000..3cee17554 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/SwiftClassSupportTests.swift @@ -0,0 +1,13 @@ +import XCTest +@_spi(Experimental) import JavaScriptKit + +@JSClass struct SwiftClassSupportImports { + @JSFunction static func jsRoundTripGreeter(_ greeter: Greeter) throws(JSException) -> Greeter +} + +final class SwiftClassSupportTests: XCTestCase { + func testRoundTripGreeter() throws { + let greeter = try SwiftClassSupportImports.jsRoundTripGreeter(Greeter(name: "Hello")) + XCTAssertEqual(greeter.name, "Hello") + } +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 95573b34e..efb168a6d 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -6,6 +6,7 @@ import { import { ImportedFoo } from './BridgeJSRuntimeTests/JavaScript/Types.mjs'; import { runJsOptionalSupportTests } from './BridgeJSRuntimeTests/JavaScript/OptionalSupportTests.mjs'; import { getImports as getClosureSupportImports } from './BridgeJSRuntimeTests/JavaScript/ClosureSupportTests.mjs'; +import { getImports as getSwiftClassSupportImports } from './BridgeJSRuntimeTests/JavaScript/SwiftClassSupportTests.mjs'; /** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ export async function setupOptions(options, context) { @@ -213,6 +214,7 @@ export async function setupOptions(options, context) { runJsOptionalSupportTests(exports); }, ClosureSupportImports: getClosureSupportImports(importsContext), + SwiftClassSupportImports: getSwiftClassSupportImports(importsContext), }; }, addToCoreImports(importObject, importsContext) {