Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ public final class SwiftToSkeleton {
var perSourceErrors: [(inputFilePath: String, errors: [DiagnosticError])] = []
var importedFiles: [ImportedFileSkeleton] = []
var exported = ExportedSkeleton(functions: [], classes: [], enums: [], exposeToGlobal: exposeToGlobal)
var exportCollectors: [ExportSwiftAPICollector] = []

for (sourceFile, inputFilePath) in sourceFiles {
progress.print("Processing \(inputFilePath)")

let exportCollector = ExportSwiftAPICollector(parent: self)
exportCollector.walk(sourceFile)
exportCollectors.append(exportCollector)

let typeNameCollector = ImportSwiftMacrosJSImportTypeNameCollector(viewMode: .sourceAccurate)
typeNameCollector.walk(sourceFile)
Expand All @@ -74,7 +76,15 @@ public final class SwiftToSkeleton {
if !importedFile.isEmpty {
importedFiles.append(importedFile)
}
exportCollector.finalize(&exported)
}

// Resolve extensions against all collectors. This needs to happen at this point so we can resolve both same file and cross file extensions.
for source in exportCollectors {
source.resolveDeferredExtensions(against: exportCollectors)
}

for collector in exportCollectors {
collector.finalize(&exported)
}

if !perSourceErrors.isEmpty {
Expand Down Expand Up @@ -486,6 +496,8 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
var exportedStructNames: [String] = []
var exportedStructByName: [String: ExportedStruct] = [:]
var errors: [DiagnosticError] = []
/// Extensions collected during the walk, to be resolved after all files have been walked
var deferredExtensions: [ExtensionDeclSyntax] = []

func finalize(_ result: inout ExportedSkeleton) {
result.functions.append(contentsOf: exportedFunctions)
Expand Down Expand Up @@ -1388,6 +1400,52 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor {
}
}

override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
// Defer until all type declarations in the module have been collected.
deferredExtensions.append(node)
return .skipChildren
}

func resolveDeferredExtensions(against collectors: [ExportSwiftAPICollector]) {
for ext in deferredExtensions {
var resolved = false
for collector in collectors {
if collector.resolveExtension(ext) {
resolved = true
break
}
}
if !resolved {
diagnose(
node: ext.extendedType,
message: "Unsupported type '\(ext.extendedType.trimmedDescription)'.",
hint: "You can only extend `@JS` annotated types defined in the same module"
)
}
}
}

/// Walks extension members under the matching type’s state, returning whether the type was found
func resolveExtension(_ ext: ExtensionDeclSyntax) -> Bool {
let name = ext.extendedType.trimmedDescription
let state: State
if let entry = exportedClassByName.first(where: { $0.value.name == name }) {
state = .classBody(name: name, key: entry.key)
} else if let entry = exportedStructByName.first(where: { $0.value.name == name }) {
state = .structBody(name: name, key: entry.key)
} else if let entry = exportedEnumByName.first(where: { $0.value.name == name }) {
state = .enumBody(name: name, key: entry.key)
} else {
return false
}
stateStack.push(state: state)
for member in ext.memberBlock.members {
walk(member)
}
stateStack.pop()
return true
}

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
guard let jsAttribute = node.attributes.firstJSAttribute else {
return .skipChildren
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ import Testing
try snapshotCodegen(skeleton: skeleton, name: "CrossFileFunctionTypes.ReverseOrder")
}

@Test
func codegenCrossFileExtension() throws {
let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
let classURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileExtensionClass.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: classURL, encoding: .utf8)),
inputFilePath: "CrossFileExtensionClass.swift"
)
let extensionURL = Self.multifileInputsDirectory.appendingPathComponent("CrossFileExtension.swift")
swiftAPI.addSourceFile(
Parser.parse(source: try String(contentsOf: extensionURL, encoding: .utf8)),
inputFilePath: "CrossFileExtension.swift"
)
let skeleton = try swiftAPI.finalize()
try snapshotCodegen(skeleton: skeleton, name: "CrossFileExtension")
}


@Test
func codegenSkipsEmptySkeletons() throws {
let swiftAPI = SwiftToSkeleton(progress: .silent, moduleName: "TestModule", exposeToGlobal: false)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extension Greeter {
@JS func greetFormally() -> String {
return "Good day, " + self.name + "."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@JS class Greeter {
@JS init(name: String) {}
@JS func greet() -> String { return "" }
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,15 @@ enum APIResult {
}
}
}

extension MathUtils {
@JS static func divide(a: Int, b: Int) -> Int {
return a / b
}
}

extension Calculator {
@JS static func cube(value: Int) -> Int {
return value * value * value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
}
}

extension Greeter {
@JS func greetEnthusiastically() -> String {
return "Hey, " + self.name + "!!!"
}
}

@JS func takeGreeter(greeter: Greeter) {
print(greeter.greet())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,9 @@
}

@JS func roundtripContainer(_ container: Container) -> Container

extension DataPoint {
@JS func distanceFromOrigin() -> Double {
return (x * x + y * y).squareRoot()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"exported" : {
"classes" : [
{
"constructor" : {
"abiName" : "bjs_Greeter_init",
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : false
},
"parameters" : [
{
"label" : "name",
"name" : "name",
"type" : {
"string" : {

}
}
}
]
},
"methods" : [
{
"abiName" : "bjs_Greeter_greet",
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : false
},
"name" : "greet",
"parameters" : [

],
"returnType" : {
"string" : {

}
}
},
{
"abiName" : "bjs_Greeter_greetFormally",
"effects" : {
"isAsync" : false,
"isStatic" : false,
"isThrows" : false
},
"name" : "greetFormally",
"parameters" : [

],
"returnType" : {
"string" : {

}
}
}
],
"name" : "Greeter",
"properties" : [

],
"swiftCallName" : "Greeter"
}
],
"enums" : [

],
"exposeToGlobal" : false,
"functions" : [

],
"protocols" : [

],
"structs" : [

]
},
"moduleName" : "TestModule"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@_expose(wasm, "bjs_Greeter_init")
@_cdecl("bjs_Greeter_init")
public func _bjs_Greeter_init(_ nameBytes: Int32, _ nameLength: Int32) -> UnsafeMutableRawPointer {
#if arch(wasm32)
let ret = Greeter(name: String.bridgeJSLiftParameter(nameBytes, nameLength))
return ret.bridgeJSLowerReturn()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_Greeter_greet")
@_cdecl("bjs_Greeter_greet")
public func _bjs_Greeter_greet(_ _self: UnsafeMutableRawPointer) -> Void {
#if arch(wasm32)
let ret = Greeter.bridgeJSLiftParameter(_self).greet()
return ret.bridgeJSLowerReturn()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_Greeter_greetFormally")
@_cdecl("bjs_Greeter_greetFormally")
public func _bjs_Greeter_greetFormally(_ _self: UnsafeMutableRawPointer) -> Void {
#if arch(wasm32)
let ret = Greeter.bridgeJSLiftParameter(_self).greetFormally()
return ret.bridgeJSLowerReturn()
#else
fatalError("Only available on WebAssembly")
#endif
}

@_expose(wasm, "bjs_Greeter_deinit")
@_cdecl("bjs_Greeter_deinit")
public func _bjs_Greeter_deinit(_ pointer: UnsafeMutableRawPointer) -> Void {
#if arch(wasm32)
Unmanaged<Greeter>.fromOpaque(pointer).release()
#else
fatalError("Only available on WebAssembly")
#endif
}

extension Greeter: ConvertibleToJSValue, _BridgedSwiftHeapObject {
var jsValue: JSValue {
return .object(JSObject(id: UInt32(bitPattern: _bjs_Greeter_wrap(Unmanaged.passRetained(self).toOpaque()))))
}
}

#if arch(wasm32)
@_extern(wasm, module: "TestModule", name: "bjs_Greeter_wrap")
fileprivate func _bjs_Greeter_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32
#else
fileprivate func _bjs_Greeter_wrap_extern(_ pointer: UnsafeMutableRawPointer) -> Int32 {
fatalError("Only available on WebAssembly")
}
#endif
@inline(never) fileprivate func _bjs_Greeter_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 {
return _bjs_Greeter_wrap_extern(pointer)
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,45 @@

}
}
},
{
"abiName" : "bjs_MathUtils_static_divide",
"effects" : {
"isAsync" : false,
"isStatic" : true,
"isThrows" : false
},
"name" : "divide",
"parameters" : [
{
"label" : "a",
"name" : "a",
"type" : {
"int" : {

}
}
},
{
"label" : "b",
"name" : "b",
"type" : {
"int" : {

}
}
}
],
"returnType" : {
"int" : {

}
},
"staticContext" : {
"className" : {
"_0" : "MathUtils"
}
}
}
],
"name" : "MathUtils",
Expand Down Expand Up @@ -182,6 +221,36 @@
"_0" : "Calculator"
}
}
},
{
"abiName" : "bjs_Calculator_static_cube",
"effects" : {
"isAsync" : false,
"isStatic" : true,
"isThrows" : false
},
"name" : "cube",
"parameters" : [
{
"label" : "value",
"name" : "value",
"type" : {
"int" : {

}
}
}
],
"returnType" : {
"int" : {

}
},
"staticContext" : {
"enumName" : {
"_0" : "Calculator"
}
}
}
],
"staticProperties" : [
Expand Down
Loading
Loading