Skip to content

Commit f633ef2

Browse files
BridgeJS: Generate ConvertibleToJSValue extensions for exported Swift classes
@js classes now automatically support conversion to JSValue, enabling seamless interoperability between Swift and JavaScript.
1 parent 72f26a6 commit f633ef2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+548
-31
lines changed

Benchmarks/Sources/Generated/JavaScript/BridgeJS.ExportSwift.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
}
2020
}
2121
}
22-
]
22+
],
23+
"moduleName" : "Benchmarks"
2324
}

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/BridgeJS.ExportSwift.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ public func _bjs_PlayBridgeJS_deinit(pointer: UnsafeMutableRawPointer) {
5656
Unmanaged<PlayBridgeJS>.fromOpaque(pointer).release()
5757
}
5858

59+
extension PlayBridgeJS: ConvertibleToJSValue {
60+
var jsValue: JSValue {
61+
@_extern(wasm, module: "PlayBridgeJS", name: "bjs_PlayBridgeJS_wrap")
62+
func _bjs_PlayBridgeJS_wrap(_: UnsafeMutableRawPointer) -> Int32
63+
return .object(JSObject(id: UInt32(bitPattern: _bjs_PlayBridgeJS_wrap(Unmanaged.passRetained(self).toOpaque()))))
64+
}
65+
}
66+
5967
@_expose(wasm, "bjs_PlayBridgeJSOutput_outputJs")
6068
@_cdecl("bjs_PlayBridgeJSOutput_outputJs")
6169
public func _bjs_PlayBridgeJSOutput_outputJs(_self: UnsafeMutableRawPointer) -> Void {
@@ -112,4 +120,12 @@ public func _bjs_PlayBridgeJSOutput_exportSwiftGlue(_self: UnsafeMutableRawPoint
112120
@_cdecl("bjs_PlayBridgeJSOutput_deinit")
113121
public func _bjs_PlayBridgeJSOutput_deinit(pointer: UnsafeMutableRawPointer) {
114122
Unmanaged<PlayBridgeJSOutput>.fromOpaque(pointer).release()
123+
}
124+
125+
extension PlayBridgeJSOutput: ConvertibleToJSValue {
126+
var jsValue: JSValue {
127+
@_extern(wasm, module: "PlayBridgeJS", name: "bjs_PlayBridgeJSOutput_wrap")
128+
func _bjs_PlayBridgeJSOutput_wrap(_: UnsafeMutableRawPointer) -> Int32
129+
return .object(JSObject(id: UInt32(bitPattern: _bjs_PlayBridgeJSOutput_wrap(Unmanaged.passRetained(self).toOpaque()))))
130+
}
115131
}

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.ExportSwift.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,6 @@
120120
],
121121
"functions" : [
122122

123-
]
123+
],
124+
"moduleName" : "PlayBridgeJS"
124125
}

Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ struct BridgeJSBuildPlugin: BuildToolPlugin {
4242
executable: try context.tool(named: "BridgeJSTool").url,
4343
arguments: [
4444
"export",
45+
"--module-name",
46+
target.name,
4547
"--output-skeleton",
4648
outputSkeletonPath.path,
4749
"--output-swift",

Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ extension BridgeJSCommandPlugin.Context {
105105
try runBridgeJSTool(
106106
arguments: [
107107
"export",
108+
"--module-name",
109+
target.name,
108110
"--output-skeleton",
109111
generatedJavaScriptDirectory.appending(path: "BridgeJS.ExportSwift.json").path,
110112
"--output-swift",

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import BridgeJSSkeleton
1616
/// JavaScript glue code and TypeScript definitions.
1717
public class ExportSwift {
1818
let progress: ProgressReporting
19+
let moduleName: String
1920

2021
private var exportedFunctions: [ExportedFunction] = []
2122
private var exportedClasses: [ExportedClass] = []
2223
private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver()
2324

24-
public init(progress: ProgressReporting) {
25+
public init(progress: ProgressReporting, moduleName: String) {
2526
self.progress = progress
27+
self.moduleName = moduleName
2628
}
2729

2830
/// Processes a Swift source file to find declarations marked with @JS
@@ -53,7 +55,7 @@ public class ExportSwift {
5355
}
5456
return (
5557
outputSwift: outputSwift,
56-
outputSkeleton: ExportedSkeleton(functions: exportedFunctions, classes: exportedClasses)
58+
outputSkeleton: ExportedSkeleton(moduleName: moduleName, functions: exportedFunctions, classes: exportedClasses)
5759
)
5860
}
5961

@@ -676,8 +678,41 @@ public class ExportSwift {
676678
)
677679
}
678680

681+
// Generate ConvertibleToJSValue extension
682+
decls.append(renderConvertibleToJSValueExtension(klass: klass))
683+
679684
return decls
680685
}
686+
687+
/// Generates a ConvertibleToJSValue extension for the exported class
688+
///
689+
/// # Example
690+
///
691+
/// For a class named `Greeter`, this generates:
692+
///
693+
/// ```swift
694+
/// extension Greeter: ConvertibleToJSValue {
695+
/// var jsValue: JSValue {
696+
/// @_extern(wasm, module: "MyModule", name: "bjs_Greeter_wrap")
697+
/// func _bjs_Greeter_wrap(_: UnsafeMutableRawPointer) -> Int32
698+
/// return JSObject(id: UInt32(bitPattern: _bjs_Greeter_wrap(Unmanaged.passRetained(self).toOpaque())))
699+
/// }
700+
/// }
701+
/// ```
702+
func renderConvertibleToJSValueExtension(klass: ExportedClass) -> DeclSyntax {
703+
let wrapFunctionName = "_bjs_\(klass.name)_wrap"
704+
let externFunctionName = "bjs_\(klass.name)_wrap"
705+
706+
return """
707+
extension \(raw: klass.name): ConvertibleToJSValue {
708+
var jsValue: JSValue {
709+
@_extern(wasm, module: "\(raw: moduleName)", name: "\(raw: externFunctionName)")
710+
func \(raw: wrapFunctionName)(_: UnsafeMutableRawPointer) -> Int32
711+
return .object(JSObject(id: UInt32(bitPattern: \(raw: wrapFunctionName)(Unmanaged.passRetained(self).toOpaque()))))
712+
}
713+
}
714+
"""
715+
}
681716
}
682717

683718
extension AttributeListSyntax {

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ struct BridgeJSLink {
203203
bjs["swift_js_release"] = function(id) {
204204
swift.memory.release(id);
205205
}
206+
\(renderSwiftClassWrappers().map { $0.indent(count: 12) }.joined(separator: "\n"))
206207
\(importObjectBuilders.flatMap { $0.importedLines }.map { $0.indent(count: 12) }.joined(separator: "\n"))
207208
},
208209
setInstance: (i) => {
@@ -249,6 +250,39 @@ struct BridgeJSLink {
249250
return (outputJs, outputDts)
250251
}
251252

253+
private func renderSwiftClassWrappers() -> [String] {
254+
var wrapperLines: [String] = []
255+
var modulesByName: [String: [ExportedClass]] = [:]
256+
257+
// Group classes by their module name
258+
for skeleton in exportedSkeletons {
259+
if skeleton.classes.isEmpty { continue }
260+
261+
if modulesByName[skeleton.moduleName] == nil {
262+
modulesByName[skeleton.moduleName] = []
263+
}
264+
modulesByName[skeleton.moduleName]?.append(contentsOf: skeleton.classes)
265+
}
266+
267+
// Generate wrapper functions for each module
268+
for (moduleName, classes) in modulesByName {
269+
wrapperLines.append("// Wrapper functions for module: \(moduleName)")
270+
wrapperLines.append("if (!importObject[\"\(moduleName)\"]) {")
271+
wrapperLines.append(" importObject[\"\(moduleName)\"] = {};")
272+
wrapperLines.append("}")
273+
274+
for klass in classes {
275+
let wrapperFunctionName = "bjs_\(klass.name)_wrap"
276+
wrapperLines.append("importObject[\"\(moduleName)\"][\"\(wrapperFunctionName)\"] = function(pointer) {")
277+
wrapperLines.append(" const obj = \(klass.name).__construct(pointer);")
278+
wrapperLines.append(" return swift.memory.retain(obj);")
279+
wrapperLines.append("};")
280+
}
281+
}
282+
283+
return wrapperLines
284+
}
285+
252286
private func generateImportedTypeDefinitions() -> [String] {
253287
var typeDefinitions: [String] = []
254288

@@ -736,7 +770,7 @@ struct BridgeJSLink {
736770

737771
init(moduleName: String) {
738772
self.moduleName = moduleName
739-
importedLines.append("const \(moduleName) = importObject[\"\(moduleName)\"] = {};")
773+
importedLines.append("const \(moduleName) = importObject[\"\(moduleName)\"] = importObject[\"\(moduleName)\"] || {};")
740774
}
741775

742776
func assignToImportObject(name: String, function: [String]) {

Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ public struct ExportedConstructor: Codable {
9393
}
9494

9595
public struct ExportedSkeleton: Codable {
96+
public let moduleName: String
9697
public let functions: [ExportedFunction]
9798
public let classes: [ExportedClass]
9899

99-
public init(functions: [ExportedFunction], classes: [ExportedClass]) {
100+
public init(moduleName: String, functions: [ExportedFunction], classes: [ExportedClass]) {
101+
self.moduleName = moduleName
100102
self.functions = functions
101103
self.classes = classes
102104
}

Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ import TS2Skeleton
149149
let parser = ArgumentParser(
150150
singleDashOptions: [:],
151151
doubleDashOptions: [
152+
"module-name": OptionRule(
153+
help: "The name of the module for external function references",
154+
required: true
155+
),
152156
"output-skeleton": OptionRule(
153157
help: "The output file path for the skeleton of the exported Swift APIs",
154158
required: true
@@ -168,7 +172,7 @@ import TS2Skeleton
168172
arguments: Array(arguments.dropFirst())
169173
)
170174
let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true")
171-
let exporter = ExportSwift(progress: progress)
175+
let exporter = ExportSwift(progress: progress, moduleName: doubleDashOptions["module-name"]!)
172176
for inputFile in positionalArguments.sorted() {
173177
let sourceURL = URL(fileURLWithPath: inputFile)
174178
guard sourceURL.pathExtension == "swift" else { continue }

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import Testing
4646
func snapshotExport(input: String) throws {
4747
let url = Self.inputsDirectory.appendingPathComponent(input)
4848
let sourceFile = Parser.parse(source: try String(contentsOf: url, encoding: .utf8))
49-
let swiftAPI = ExportSwift(progress: .silent)
49+
let swiftAPI = ExportSwift(progress: .silent, moduleName: "TestModule")
5050
try swiftAPI.addSourceFile(sourceFile, input)
5151
let name = url.deletingPathExtension().lastPathComponent
5252

0 commit comments

Comments
 (0)