Skip to content

Commit 0228bc6

Browse files
committed
BridgeJS: Add support for exporting structs using a box
1 parent 43ed78f commit 0228bc6

21 files changed

Lines changed: 6121 additions & 1885 deletions

File tree

Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift

Lines changed: 113 additions & 39 deletions
Large diffs are not rendered by default.

Plugins/BridgeJS/Sources/BridgeJSCore/ExternalModuleIndex.swift

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,13 @@ public struct ExternalModuleIndex {
4040
}
4141

4242
for klass in exported.classes {
43-
register(dotPath: klass.swiftCallName, bridgeType: .swiftHeapObject(klass.swiftCallName))
43+
register(dotPath: klass.swiftCallName, bridgeType: klass.bridgeType)
4444
}
4545
for structDef in exported.structs {
46-
register(dotPath: structDef.swiftCallName, bridgeType: .swiftStruct(structDef.swiftCallName))
46+
register(dotPath: structDef.swiftCallName, bridgeType: structDef.bridgeType)
4747
}
4848
for enumDef in exported.enums {
49-
let bridgeType: BridgeType
50-
switch enumDef.enumType {
51-
case .simple:
52-
bridgeType = .caseEnum(enumDef.swiftCallName)
53-
case .rawValue:
54-
guard let rawType = enumDef.rawType else { continue }
55-
bridgeType = .rawValueEnum(enumDef.swiftCallName, rawType)
56-
case .associatedValue:
57-
bridgeType = .associatedValueEnum(enumDef.swiftCallName)
58-
case .namespace:
59-
bridgeType = .namespaceEnum(enumDef.swiftCallName)
60-
}
49+
guard let bridgeType = enumDef.bridgeType else { continue }
6150
register(dotPath: enumDef.swiftCallName, bridgeType: bridgeType)
6251
}
6352
for proto in exported.protocols {

Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -918,7 +918,7 @@ extension BridgeType {
918918
return LoweringParameterInfo(loweredParameters: [("funcRef", .i32)])
919919
case .unsafePointer:
920920
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
921-
case .swiftHeapObject:
921+
case .swiftHeapObject, .swiftBoxedStruct:
922922
return LoweringParameterInfo(loweredParameters: [("pointer", .pointer)])
923923
case .swiftProtocol:
924924
switch context {
@@ -997,7 +997,7 @@ extension BridgeType {
997997
return LiftingReturnInfo(valueToLift: .i32)
998998
case .unsafePointer:
999999
return LiftingReturnInfo(valueToLift: .pointer)
1000-
case .swiftHeapObject:
1000+
case .swiftHeapObject, .swiftBoxedStruct:
10011001
return LiftingReturnInfo(valueToLift: .pointer)
10021002
case .swiftProtocol:
10031003
switch context {

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,11 @@ public final class SwiftToSkeleton {
366366
if structDecl.attributes.hasAttribute(name: "JSClass") {
367367
return .jsObject(swiftCallName)
368368
}
369+
if let jsAttribute = structDecl.attributes.firstJSAttribute,
370+
SwiftToSkeleton.extractStructStyle(from: jsAttribute) == .reference
371+
{
372+
return .swiftBoxedStruct(swiftCallName)
373+
}
369374
return .swiftStruct(swiftCallName)
370375
}
371376

@@ -523,6 +528,22 @@ public final class SwiftToSkeleton {
523528
}
524529
}
525530

531+
static func extractStructStyle(from jsAttribute: AttributeSyntax) -> JSStructStyle? {
532+
guard let arguments = jsAttribute.arguments?.as(LabeledExprListSyntax.self),
533+
let styleArg = arguments.first(where: { $0.label?.text == "structStyle" })
534+
else {
535+
return nil
536+
}
537+
let text = styleArg.expression.trimmedDescription
538+
if text.contains("reference") {
539+
return .reference
540+
}
541+
if text.contains("fields") {
542+
return .fields
543+
}
544+
return nil
545+
}
546+
526547
/// Strips surrounding backticks from an identifier (e.g. "`Foo`" -> "Foo").
527548
static func normalizeIdentifier(_ name: String) -> String {
528549
guard name.hasPrefix("`"), name.hasSuffix("`"), name.count >= 2 else {
@@ -1397,8 +1418,17 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
13971418
let structAbiName = exportedStructByName[structKey]?.abiName ?? "unknown"
13981419
staticContext = .structName(structAbiName)
13991420
} else {
1400-
diagnose(node: node, message: "@JS var must be static in structs (instance fields don't need @JS)")
1401-
return .skipChildren
1421+
switch exportedStructByName[structKey]?.structStyle ?? .default {
1422+
case .reference:
1423+
staticContext = nil
1424+
case .fields:
1425+
diagnose(
1426+
node: node,
1427+
message: "@JS var must be static in structs (instance fields don't need @JS)",
1428+
hint: "Export the struct using @JS(structStyle: .reference) instead"
1429+
)
1430+
return .skipChildren
1431+
}
14021432
}
14031433
}
14041434

@@ -1661,12 +1691,12 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
16611691
for associatedValue in enumCase.associatedValues {
16621692
switch associatedValue.type {
16631693
case .string, .integer, .float, .double, .bool, .caseEnum, .rawValueEnum,
1664-
.swiftStruct, .swiftHeapObject, .jsObject, .associatedValueEnum, .array:
1694+
.swiftStruct, .swiftHeapObject, .swiftBoxedStruct, .jsObject, .associatedValueEnum, .array:
16651695
break
16661696
case .nullable(let wrappedType, _):
16671697
switch wrappedType {
16681698
case .string, .integer, .float, .double, .bool, .caseEnum, .rawValueEnum,
1669-
.swiftStruct, .swiftHeapObject, .jsObject, .associatedValueEnum, .array:
1699+
.swiftStruct, .swiftHeapObject, .swiftBoxedStruct, .jsObject, .associatedValueEnum, .array:
16701700
break
16711701
default:
16721702
diagnose(
@@ -1772,54 +1802,61 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
17721802
for: node,
17731803
message: "Struct visibility must be at least internal"
17741804
)
1805+
let structStyle = SwiftToSkeleton.extractStructStyle(from: jsAttribute)
17751806

17761807
var properties: [ExportedProperty] = []
17771808

1778-
// Process all variables in struct as readonly (value semantics) and don't require @JS
1779-
for member in node.memberBlock.members {
1780-
if let varDecl = member.decl.as(VariableDeclSyntax.self) {
1781-
let isStatic = varDecl.modifiers.contains { modifier in
1782-
modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
1783-
}
1784-
1785-
// Handled with error in visitVariable
1786-
if varDecl.attributes.hasJSAttribute() {
1787-
continue
1788-
}
1789-
// Skips static non-@JS properties
1790-
if isStatic {
1791-
continue
1792-
}
1793-
1794-
for binding in varDecl.bindings {
1795-
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
1796-
continue
1809+
switch structStyle ?? .default {
1810+
case .reference:
1811+
// Reference structs require @JS on variables to export
1812+
break
1813+
case .fields:
1814+
// Process all variables in struct as readonly (value semantics) and don't require @JS
1815+
for member in node.memberBlock.members {
1816+
if let varDecl = member.decl.as(VariableDeclSyntax.self) {
1817+
let isStatic = varDecl.modifiers.contains { modifier in
1818+
modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
17971819
}
17981820

1799-
let fieldName = pattern.identifier.text
1800-
1801-
guard let typeAnnotation = binding.typeAnnotation else {
1802-
diagnose(node: binding, message: "Struct field must have explicit type annotation")
1821+
// Handled with error in visitVariable
1822+
if varDecl.attributes.hasJSAttribute() {
18031823
continue
18041824
}
1805-
1806-
guard
1807-
let fieldType = withLookupErrors({
1808-
self.parent.lookupType(for: typeAnnotation.type, errors: &$0)
1809-
})
1810-
else {
1825+
// Skips static non-@JS properties
1826+
if isStatic {
18111827
continue
18121828
}
18131829

1814-
let property = ExportedProperty(
1815-
name: fieldName,
1816-
type: fieldType,
1817-
isReadonly: true,
1818-
isStatic: false,
1819-
namespace: effectiveNamespace,
1820-
staticContext: nil
1821-
)
1822-
properties.append(property)
1830+
for binding in varDecl.bindings {
1831+
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
1832+
continue
1833+
}
1834+
1835+
let fieldName = pattern.identifier.text
1836+
1837+
guard let typeAnnotation = binding.typeAnnotation else {
1838+
diagnose(node: binding, message: "Struct field must have explicit type annotation")
1839+
continue
1840+
}
1841+
1842+
guard
1843+
let fieldType = withLookupErrors({
1844+
self.parent.lookupType(for: typeAnnotation.type, errors: &$0)
1845+
})
1846+
else {
1847+
continue
1848+
}
1849+
1850+
let property = ExportedProperty(
1851+
name: fieldName,
1852+
type: fieldType,
1853+
isReadonly: true,
1854+
isStatic: false,
1855+
namespace: effectiveNamespace,
1856+
staticContext: nil
1857+
)
1858+
properties.append(property)
1859+
}
18231860
}
18241861
}
18251862
}
@@ -1831,7 +1868,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18311868
explicitAccessControl: explicitAccessControl,
18321869
properties: properties,
18331870
methods: [],
1834-
namespace: effectiveNamespace
1871+
namespace: effectiveNamespace,
1872+
structStyle: structStyle
18351873
)
18361874

18371875
exportedStructByName[structUniqueKey] = exportedStruct
@@ -1858,6 +1896,11 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
18581896
return
18591897
}
18601898

1899+
guard exportedStruct.structStyle ?? .default == .fields else {
1900+
// This validation is only required for fields structs.
1901+
return
1902+
}
1903+
18611904
let instanceProps = exportedStruct.properties.filter { !$0.isStatic }
18621905
let expectedLabels = instanceProps.map(\.name)
18631906
let actualLabels = constructor.parameters.compactMap(\.label)

0 commit comments

Comments
 (0)