Skip to content

Commit da51c40

Browse files
BridgeJS: allow @js structs to copy from JSObject
Add struct-level helpers for copying values out of a backing JS object and raising a new JS object from a Swift struct. This makes it possible to round-trip JSON-style plain objects without manual field mapping, while keeping the default value semantics of exported structs.
1 parent 7b0ef78 commit da51c40

File tree

57 files changed

+2023
-3
lines changed

Some content is hidden

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

57 files changed

+2023
-3
lines changed

Benchmarks/Sources/Generated/BridgeJS.swift

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,49 @@ extension SimpleStruct: _BridgedSwiftStruct {
267267
_swift_js_push_f32(self.rate)
268268
_swift_js_push_f64(self.precise)
269269
}
270+
271+
init(unsafelyCopying jsObject: JSObject) {
272+
let __bjs_cleanupId = _SimpleStructHelpers.lower(jsObject)
273+
defer {
274+
_swift_js_struct_cleanup(__bjs_cleanupId)
275+
}
276+
self = Self.bridgeJSLiftParameter()
277+
}
278+
279+
func toJSObject() -> JSObject {
280+
var __bjs_self = self
281+
__bjs_self.bridgeJSLowerReturn()
282+
return _SimpleStructHelpers.raise()
283+
}
284+
}
285+
286+
fileprivate enum _SimpleStructHelpers {
287+
static func lower(_ jsObject: JSObject) -> Int32 {
288+
return _bjs_struct_lower_SimpleStruct(jsObject.bridgeJSLowerParameter())
289+
}
290+
291+
static func raise() -> JSObject {
292+
return JSObject(id: UInt32(bitPattern: _bjs_struct_raise_SimpleStruct()))
293+
}
294+
}
295+
296+
#if arch(wasm32)
297+
@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_SimpleStruct")
298+
fileprivate func _bjs_struct_lower_SimpleStruct(_ objectId: Int32) -> Int32
299+
#else
300+
fileprivate func _bjs_struct_lower_SimpleStruct(_ objectId: Int32) -> Int32 {
301+
fatalError("Only available on WebAssembly")
270302
}
303+
#endif
304+
305+
#if arch(wasm32)
306+
@_extern(wasm, module: "bjs", name: "swift_js_struct_raise_SimpleStruct")
307+
fileprivate func _bjs_struct_raise_SimpleStruct() -> Int32
308+
#else
309+
fileprivate func _bjs_struct_raise_SimpleStruct() -> Int32 {
310+
fatalError("Only available on WebAssembly")
311+
}
312+
#endif
271313

272314
extension Address: _BridgedSwiftStruct {
273315
@_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter() -> Address {
@@ -288,7 +330,49 @@ extension Address: _BridgedSwiftStruct {
288330
}
289331
_swift_js_push_int(Int32(self.zipCode))
290332
}
333+
334+
init(unsafelyCopying jsObject: JSObject) {
335+
let __bjs_cleanupId = _AddressHelpers.lower(jsObject)
336+
defer {
337+
_swift_js_struct_cleanup(__bjs_cleanupId)
338+
}
339+
self = Self.bridgeJSLiftParameter()
340+
}
341+
342+
func toJSObject() -> JSObject {
343+
var __bjs_self = self
344+
__bjs_self.bridgeJSLowerReturn()
345+
return _AddressHelpers.raise()
346+
}
347+
}
348+
349+
fileprivate enum _AddressHelpers {
350+
static func lower(_ jsObject: JSObject) -> Int32 {
351+
return _bjs_struct_lower_Address(jsObject.bridgeJSLowerParameter())
352+
}
353+
354+
static func raise() -> JSObject {
355+
return JSObject(id: UInt32(bitPattern: _bjs_struct_raise_Address()))
356+
}
357+
}
358+
359+
#if arch(wasm32)
360+
@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Address")
361+
fileprivate func _bjs_struct_lower_Address(_ objectId: Int32) -> Int32
362+
#else
363+
fileprivate func _bjs_struct_lower_Address(_ objectId: Int32) -> Int32 {
364+
fatalError("Only available on WebAssembly")
291365
}
366+
#endif
367+
368+
#if arch(wasm32)
369+
@_extern(wasm, module: "bjs", name: "swift_js_struct_raise_Address")
370+
fileprivate func _bjs_struct_raise_Address() -> Int32
371+
#else
372+
fileprivate func _bjs_struct_raise_Address() -> Int32 {
373+
fatalError("Only available on WebAssembly")
374+
}
375+
#endif
292376

293377
extension Person: _BridgedSwiftStruct {
294378
@_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter() -> Person {
@@ -315,7 +399,49 @@ extension Person: _BridgedSwiftStruct {
315399
}
316400
_swift_js_push_int(__bjs_isSome_email ? 1 : 0)
317401
}
402+
403+
init(unsafelyCopying jsObject: JSObject) {
404+
let __bjs_cleanupId = _PersonHelpers.lower(jsObject)
405+
defer {
406+
_swift_js_struct_cleanup(__bjs_cleanupId)
407+
}
408+
self = Self.bridgeJSLiftParameter()
409+
}
410+
411+
func toJSObject() -> JSObject {
412+
var __bjs_self = self
413+
__bjs_self.bridgeJSLowerReturn()
414+
return _PersonHelpers.raise()
415+
}
416+
}
417+
418+
fileprivate enum _PersonHelpers {
419+
static func lower(_ jsObject: JSObject) -> Int32 {
420+
return _bjs_struct_lower_Person(jsObject.bridgeJSLowerParameter())
421+
}
422+
423+
static func raise() -> JSObject {
424+
return JSObject(id: UInt32(bitPattern: _bjs_struct_raise_Person()))
425+
}
426+
}
427+
428+
#if arch(wasm32)
429+
@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_Person")
430+
fileprivate func _bjs_struct_lower_Person(_ objectId: Int32) -> Int32
431+
#else
432+
fileprivate func _bjs_struct_lower_Person(_ objectId: Int32) -> Int32 {
433+
fatalError("Only available on WebAssembly")
318434
}
435+
#endif
436+
437+
#if arch(wasm32)
438+
@_extern(wasm, module: "bjs", name: "swift_js_struct_raise_Person")
439+
fileprivate func _bjs_struct_raise_Person() -> Int32
440+
#else
441+
fileprivate func _bjs_struct_raise_Person() -> Int32 {
442+
fatalError("Only available on WebAssembly")
443+
}
444+
#endif
319445

320446
extension ComplexStruct: _BridgedSwiftStruct {
321447
@_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter() -> ComplexStruct {
@@ -345,7 +471,49 @@ extension ComplexStruct: _BridgedSwiftStruct {
345471
_swift_js_push_string(ptr.baseAddress, Int32(ptr.count))
346472
}
347473
}
474+
475+
init(unsafelyCopying jsObject: JSObject) {
476+
let __bjs_cleanupId = _ComplexStructHelpers.lower(jsObject)
477+
defer {
478+
_swift_js_struct_cleanup(__bjs_cleanupId)
479+
}
480+
self = Self.bridgeJSLiftParameter()
481+
}
482+
483+
func toJSObject() -> JSObject {
484+
var __bjs_self = self
485+
__bjs_self.bridgeJSLowerReturn()
486+
return _ComplexStructHelpers.raise()
487+
}
488+
}
489+
490+
fileprivate enum _ComplexStructHelpers {
491+
static func lower(_ jsObject: JSObject) -> Int32 {
492+
return _bjs_struct_lower_ComplexStruct(jsObject.bridgeJSLowerParameter())
493+
}
494+
495+
static func raise() -> JSObject {
496+
return JSObject(id: UInt32(bitPattern: _bjs_struct_raise_ComplexStruct()))
497+
}
498+
}
499+
500+
#if arch(wasm32)
501+
@_extern(wasm, module: "bjs", name: "swift_js_struct_lower_ComplexStruct")
502+
fileprivate func _bjs_struct_lower_ComplexStruct(_ objectId: Int32) -> Int32
503+
#else
504+
fileprivate func _bjs_struct_lower_ComplexStruct(_ objectId: Int32) -> Int32 {
505+
fatalError("Only available on WebAssembly")
348506
}
507+
#endif
508+
509+
#if arch(wasm32)
510+
@_extern(wasm, module: "bjs", name: "swift_js_struct_raise_ComplexStruct")
511+
fileprivate func _bjs_struct_raise_ComplexStruct() -> Int32
512+
#else
513+
fileprivate func _bjs_struct_raise_ComplexStruct() -> Int32 {
514+
fatalError("Only available on WebAssembly")
515+
}
516+
#endif
349517

350518
@_expose(wasm, "bjs_run")
351519
@_cdecl("bjs_run")

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public class ExportSwift {
7474

7575
let structCodegen = StructCodegen()
7676
for structDef in skeleton.structs {
77-
decls.append(structCodegen.renderStructHelpers(structDef))
77+
decls.append(contentsOf: structCodegen.renderStructHelpers(structDef))
7878
decls.append(contentsOf: try renderSingleExportedStruct(struct: structDef))
7979
}
8080

@@ -1147,12 +1147,19 @@ struct EnumCodegen {
11471147
struct StructCodegen {
11481148
private let stackCodegen = StackCodegen()
11491149

1150-
func renderStructHelpers(_ structDef: ExportedStruct) -> DeclSyntax {
1150+
func renderStructHelpers(_ structDef: ExportedStruct) -> [DeclSyntax] {
11511151
let typeName = structDef.swiftCallName
11521152
let liftCode = generateStructLiftCode(structDef: structDef)
11531153
let lowerCode = generateStructLowerCode(structDef: structDef)
1154+
let accessControl = structDef.explicitAccessControl.map { "\($0) " } ?? ""
11541155

1155-
return """
1156+
let helpersTypeName = "_\(structDef.name)Helpers"
1157+
let lowerExternName = "swift_js_struct_lower_\(structDef.name)"
1158+
let raiseExternName = "swift_js_struct_raise_\(structDef.name)"
1159+
let lowerFunctionName = "_bjs_struct_lower_\(structDef.name)"
1160+
let raiseFunctionName = "_bjs_struct_raise_\(structDef.name)"
1161+
1162+
let bridgedStructExtension: DeclSyntax = """
11561163
extension \(raw: typeName): _BridgedSwiftStruct {
11571164
@_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter() -> \(raw: typeName) {
11581165
\(raw: liftCode.joined(separator: "\n"))
@@ -1161,8 +1168,83 @@ struct StructCodegen {
11611168
@_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() {
11621169
\(raw: lowerCode.joined(separator: "\n"))
11631170
}
1171+
1172+
\(raw: accessControl)init(unsafelyCopying jsObject: JSObject) {
1173+
let __bjs_cleanupId = \(raw: helpersTypeName).lower(jsObject)
1174+
defer { _swift_js_struct_cleanup(__bjs_cleanupId) }
1175+
self = Self.bridgeJSLiftParameter()
1176+
}
1177+
1178+
\(raw: accessControl)func toJSObject() -> JSObject {
1179+
var __bjs_self = self
1180+
__bjs_self.bridgeJSLowerReturn()
1181+
return \(raw: helpersTypeName).raise()
1182+
}
11641183
}
11651184
"""
1185+
1186+
let helpersType: DeclSyntax = """
1187+
fileprivate enum \(raw: helpersTypeName) {
1188+
static func lower(_ jsObject: JSObject) -> Int32 {
1189+
return \(raw: lowerFunctionName)(jsObject.bridgeJSLowerParameter())
1190+
}
1191+
1192+
static func raise() -> JSObject {
1193+
return JSObject(id: UInt32(bitPattern: \(raw: raiseFunctionName)()))
1194+
}
1195+
}
1196+
"""
1197+
1198+
let lowerExternDecl = Self.renderStructExtern(
1199+
externName: lowerExternName,
1200+
functionName: lowerFunctionName,
1201+
signature: SwiftSignatureBuilder.buildABIFunctionSignature(
1202+
abiParameters: [("objectId", .i32)],
1203+
returnType: .i32
1204+
)
1205+
)
1206+
let raiseExternDecl = Self.renderStructExtern(
1207+
externName: raiseExternName,
1208+
functionName: raiseFunctionName,
1209+
signature: SwiftSignatureBuilder.buildABIFunctionSignature(
1210+
abiParameters: [],
1211+
returnType: .i32
1212+
)
1213+
)
1214+
1215+
return [bridgedStructExtension, helpersType, lowerExternDecl, raiseExternDecl]
1216+
}
1217+
1218+
private static func renderStructExtern(
1219+
externName: String,
1220+
functionName: String,
1221+
signature: FunctionSignatureSyntax
1222+
) -> DeclSyntax {
1223+
let externFuncDecl = SwiftCodePattern.buildExternFunctionDecl(
1224+
moduleName: "bjs",
1225+
abiName: externName,
1226+
functionName: functionName,
1227+
signature: signature
1228+
)
1229+
1230+
let stubFuncDecl = FunctionDeclSyntax(
1231+
modifiers: DeclModifierListSyntax {
1232+
DeclModifierSyntax(name: .keyword(.fileprivate))
1233+
},
1234+
funcKeyword: .keyword(.func),
1235+
name: .identifier(functionName),
1236+
signature: signature,
1237+
body: CodeBlockSyntax {
1238+
"fatalError(\"Only available on WebAssembly\")"
1239+
}
1240+
)
1241+
1242+
return DeclSyntax(
1243+
SwiftCodePattern.buildWasmConditionalCompilationDecls(
1244+
wasmDecl: DeclSyntax(externFuncDecl),
1245+
elseDecl: DeclSyntax(stubFuncDecl)
1246+
)
1247+
)
11661248
}
11671249

11681250
private func generateStructLiftCode(structDef: ExportedStruct) -> [String] {

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ public struct BridgeJSLink {
245245
"let \(JSGlueVariableScope.reservedTmpParamF64s) = [];",
246246
"let \(JSGlueVariableScope.reservedTmpRetPointers) = [];",
247247
"let \(JSGlueVariableScope.reservedTmpParamPointers) = [];",
248+
"let \(JSGlueVariableScope.reservedTmpStructCleanups) = [];",
248249
"const \(JSGlueVariableScope.reservedEnumHelpers) = {};",
249250
"const \(JSGlueVariableScope.reservedStructHelpers) = {};",
250251
"",
@@ -295,6 +296,7 @@ public struct BridgeJSLink {
295296

296297
private func generateAddImports() -> CodeFragmentPrinter {
297298
let printer = CodeFragmentPrinter()
299+
let allStructs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
298300
printer.write("return {")
299301
printer.indent {
300302
printer.write(lines: [
@@ -427,6 +429,53 @@ public struct BridgeJSLink {
427429
printer.write("return \(JSGlueVariableScope.reservedTmpParamPointers).pop();")
428430
}
429431
printer.write("}")
432+
433+
printer.write("bjs[\"swift_js_struct_cleanup\"] = function(cleanupId) {")
434+
printer.indent {
435+
printer.write("if (cleanupId === 0) { return; }")
436+
printer.write("const index = (cleanupId | 0) - 1;")
437+
printer.write("const cleanup = \(JSGlueVariableScope.reservedTmpStructCleanups)[index];")
438+
printer.write("\(JSGlueVariableScope.reservedTmpStructCleanups)[index] = null;")
439+
printer.write("if (cleanup) { cleanup(); }")
440+
printer.write(
441+
"while (\(JSGlueVariableScope.reservedTmpStructCleanups).length > 0 && \(JSGlueVariableScope.reservedTmpStructCleanups)[\(JSGlueVariableScope.reservedTmpStructCleanups).length - 1] == null) {"
442+
)
443+
printer.indent {
444+
printer.write("\(JSGlueVariableScope.reservedTmpStructCleanups).pop();")
445+
}
446+
printer.write("}")
447+
}
448+
printer.write("}")
449+
450+
if !allStructs.isEmpty {
451+
for structDef in allStructs {
452+
printer.write("bjs[\"swift_js_struct_lower_\(structDef.name)\"] = function(objectId) {")
453+
printer.indent {
454+
printer.write(
455+
"const { cleanup: cleanup } = \(JSGlueVariableScope.reservedStructHelpers).\(structDef.name).lower(\(JSGlueVariableScope.reservedSwift).memory.getObject(objectId));"
456+
)
457+
printer.write("if (cleanup) {")
458+
printer.indent {
459+
printer.write(
460+
"return \(JSGlueVariableScope.reservedTmpStructCleanups).push(cleanup);"
461+
)
462+
}
463+
printer.write("}")
464+
printer.write("return 0;")
465+
}
466+
printer.write("}")
467+
468+
printer.write("bjs[\"swift_js_struct_raise_\(structDef.name)\"] = function() {")
469+
printer.indent {
470+
printer.write(
471+
"const value = \(JSGlueVariableScope.reservedStructHelpers).\(structDef.name).raise(\(JSGlueVariableScope.reservedTmpRetStrings), \(JSGlueVariableScope.reservedTmpRetInts), \(JSGlueVariableScope.reservedTmpRetF32s), \(JSGlueVariableScope.reservedTmpRetF64s), \(JSGlueVariableScope.reservedTmpRetPointers));"
472+
)
473+
printer.write("return \(JSGlueVariableScope.reservedSwift).memory.retain(value);")
474+
}
475+
printer.write("}")
476+
}
477+
}
478+
430479
printer.write("bjs[\"swift_js_return_optional_bool\"] = function(isSome, value) {")
431480
printer.indent {
432481
printer.write("if (isSome === 0) {")

0 commit comments

Comments
 (0)