diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd9c68493..5054ea6ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,12 +21,12 @@ jobs: target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasi" - os: ubuntu-22.04 toolchain: - download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a/swift-DEVELOPMENT-SNAPSHOT-2025-02-26-a-ubuntu22.04.tar.gz + download-url: https://download.swift.org/development/ubuntu2204/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a/swift-DEVELOPMENT-SNAPSHOT-2025-06-12-a-ubuntu22.04.tar.gz wasi-backend: Node target: "wasm32-unknown-wasip1-threads" diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift index 4d59c772e..a41a86e88 100644 --- a/Benchmarks/Package.swift +++ b/Benchmarks/Package.swift @@ -10,8 +10,11 @@ let package = Package( targets: [ .executableTarget( name: "Benchmarks", - dependencies: ["JavaScriptKit"], - exclude: ["Generated/JavaScript", "bridge.d.ts"], + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptFoundationCompat", package: "JavaScriptKit"), + ], + exclude: ["Generated/JavaScript", "bridge-js.d.ts"], swiftSettings: [ .enableExperimentalFeature("Extern") ] diff --git a/Benchmarks/Sources/Benchmarks.swift b/Benchmarks/Sources/Benchmarks.swift index 602aa843c..155acae16 100644 --- a/Benchmarks/Sources/Benchmarks.swift +++ b/Benchmarks/Sources/Benchmarks.swift @@ -1,4 +1,6 @@ import JavaScriptKit +import JavaScriptFoundationCompat +import Foundation class Benchmark { init(_ title: String) { @@ -75,4 +77,22 @@ class Benchmark { } } } + + do { + let conversion = Benchmark("Conversion") + let data = Data(repeating: 0, count: 10_000) + conversion.testSuite("Data to JSTypedArray") { + for _ in 0..<1_000_000 { + _ = data.jsTypedArray + } + } + + let uint8Array = data.jsTypedArray + + conversion.testSuite("JSTypedArray to Data") { + for _ in 0..<1_000_000 { + _ = Data.construct(from: uint8Array) + } + } + } } diff --git a/Benchmarks/Sources/Generated/ExportSwift.swift b/Benchmarks/Sources/Generated/ExportSwift.swift index a8745b649..9d4a8a9c5 100644 --- a/Benchmarks/Sources/Generated/ExportSwift.swift +++ b/Benchmarks/Sources/Generated/ExportSwift.swift @@ -3,13 +3,21 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) -@_expose(wasm, "bjs_main") -@_cdecl("bjs_main") -public func _bjs_main() -> Void { - main() +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) + +@_expose(wasm, "bjs_run") +@_cdecl("bjs_run") +public func _bjs_run() -> Void { + run() } \ No newline at end of file diff --git a/Benchmarks/Sources/Generated/ImportTS.swift b/Benchmarks/Sources/Generated/ImportTS.swift index 583b9ba58..521c49c04 100644 --- a/Benchmarks/Sources/Generated/ImportTS.swift +++ b/Benchmarks/Sources/Generated/ImportTS.swift @@ -12,9 +12,6 @@ private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 @_extern(wasm, module: "bjs", name: "init_memory_with_result") private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void - func benchmarkHelperNoop() -> Void { @_extern(wasm, module: "Benchmarks", name: "bjs_benchmarkHelperNoop") func bjs_benchmarkHelperNoop() -> Void diff --git a/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json index 0b1b70b70..f0fd49e51 100644 --- a/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json +++ b/Benchmarks/Sources/Generated/JavaScript/ExportSwift.json @@ -4,8 +4,12 @@ ], "functions" : [ { - "abiName" : "bjs_main", - "name" : "main", + "abiName" : "bjs_run", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "run", "parameters" : [ ], diff --git a/Benchmarks/Sources/bridge-js.config.json b/Benchmarks/Sources/bridge-js.config.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/Benchmarks/Sources/bridge-js.config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Benchmarks/Sources/bridge.d.ts b/Benchmarks/Sources/bridge-js.d.ts similarity index 100% rename from Benchmarks/Sources/bridge.d.ts rename to Benchmarks/Sources/bridge-js.d.ts diff --git a/Examples/ActorOnWebWorker/Sources/MyApp.swift b/Examples/ActorOnWebWorker/Sources/MyApp.swift index 357956a7e..9b38fa30c 100644 --- a/Examples/ActorOnWebWorker/Sources/MyApp.swift +++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift @@ -255,7 +255,6 @@ enum OwnedExecutor { static func main() { JavaScriptEventLoop.installGlobalExecutor() - WebWorkerTaskExecutor.installGlobalExecutor() let useDedicatedWorker = !(JSObject.global.disableDedicatedWorker.boolean ?? false) Task { diff --git a/Examples/ImportTS/Sources/bridge.d.ts b/Examples/ImportTS/Sources/bridge-js.d.ts similarity index 100% rename from Examples/ImportTS/Sources/bridge.d.ts rename to Examples/ImportTS/Sources/bridge-js.d.ts diff --git a/Examples/ImportTS/Sources/main.swift b/Examples/ImportTS/Sources/main.swift index 4328b0a3b..4853a9665 100644 --- a/Examples/ImportTS/Sources/main.swift +++ b/Examples/ImportTS/Sources/main.swift @@ -1,9 +1,9 @@ import JavaScriptKit // This function is automatically generated by the @JS plugin -// It demonstrates how to use TypeScript functions and types imported from bridge.d.ts +// It demonstrates how to use TypeScript functions and types imported from bridge-js.d.ts @JS public func run() { - // Call the imported consoleLog function defined in bridge.d.ts + // Call the imported consoleLog function defined in bridge-js.d.ts consoleLog("Hello, World!") // Get the document object - this comes from the imported getDocument() function diff --git a/Examples/Multithreading/Sources/MyApp/main.swift b/Examples/Multithreading/Sources/MyApp/main.swift index 9a1e09bb4..f9839ffde 100644 --- a/Examples/Multithreading/Sources/MyApp/main.swift +++ b/Examples/Multithreading/Sources/MyApp/main.swift @@ -3,7 +3,6 @@ import JavaScriptEventLoop import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -WebWorkerTaskExecutor.installGlobalExecutor() func renderInCanvas(ctx: JSObject, image: ImageView) { let imageData = ctx.createImageData!(image.width, image.height).object! diff --git a/Examples/OffscrenCanvas/Sources/MyApp/main.swift b/Examples/OffscrenCanvas/Sources/MyApp/main.swift index a2a6e2aac..5709c664c 100644 --- a/Examples/OffscrenCanvas/Sources/MyApp/main.swift +++ b/Examples/OffscrenCanvas/Sources/MyApp/main.swift @@ -2,7 +2,6 @@ import JavaScriptEventLoop import JavaScriptKit JavaScriptEventLoop.installGlobalExecutor() -WebWorkerTaskExecutor.installGlobalExecutor() protocol CanvasRenderer { func render(canvas: JSObject, size: Int) async throws diff --git a/Package.swift b/Package.swift index 3657bfa99..4f4ecd064 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptFoundationCompat", targets: ["JavaScriptFoundationCompat"]), .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), .plugin(name: "PackageToJS", targets: ["PackageToJS"]), .plugin(name: "BridgeJS", targets: ["BridgeJS"]), @@ -106,6 +107,18 @@ let package = Package( "JavaScriptEventLoopTestSupport", ] ), + .target( + name: "JavaScriptFoundationCompat", + dependencies: [ + "JavaScriptKit" + ] + ), + .testTarget( + name: "JavaScriptFoundationCompatTests", + dependencies: [ + "JavaScriptFoundationCompat" + ] + ), .plugin( name: "PackageToJS", capability: .command( diff --git a/Plugins/BridgeJS/README.md b/Plugins/BridgeJS/README.md index 9cbd04011..f762c294b 100644 --- a/Plugins/BridgeJS/README.md +++ b/Plugins/BridgeJS/README.md @@ -22,7 +22,7 @@ graph LR A.swift --> E1[[bridge-js export]] B.swift --> E1 E1 --> G1[ExportSwift.swift] - B1[bridge.d.ts]-->I1[[bridge-js import]] + B1[bridge-js.d.ts]-->I1[[bridge-js import]] I1 --> G2[ImportTS.swift] end I1 --> G4[ImportTS.json] @@ -32,7 +32,7 @@ graph LR C.swift --> E2[[bridge-js export]] D.swift --> E2 E2 --> G5[ExportSwift.swift] - B2[bridge.d.ts]-->I2[[bridge-js import]] + B2[bridge-js.d.ts]-->I2[[bridge-js import]] I2 --> G6[ImportTS.swift] end I2 --> G8[ImportTS.json] @@ -42,8 +42,8 @@ graph LR G7 --> L1 G8 --> L1 - L1 --> F1[bridge.js] - L1 --> F2[bridge.d.ts] + L1 --> F1[bridge-js.js] + L1 --> F2[bridge-js.d.ts] ModuleA -----> App[App.wasm] ModuleB -----> App @@ -135,3 +135,4 @@ TBD declare var Foo: FooConstructor; ``` - [ ] Use `externref` once it's widely available +- [ ] Test SwiftObject roundtrip \ No newline at end of file diff --git a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift index 4ea725ed5..c9ea8987a 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSBuildPlugin/BridgeJSBuildPlugin.swift @@ -11,17 +11,32 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { guard let swiftSourceModuleTarget = target as? SwiftSourceModuleTarget else { return [] } - return try [ - createExportSwiftCommand(context: context, target: swiftSourceModuleTarget), - createImportTSCommand(context: context, target: swiftSourceModuleTarget), - ] + var commands: [Command] = [] + commands.append(try createExportSwiftCommand(context: context, target: swiftSourceModuleTarget)) + if let importCommand = try createImportTSCommand(context: context, target: swiftSourceModuleTarget) { + commands.append(importCommand) + } + return commands + } + + private func pathToConfigFile(target: SwiftSourceModuleTarget) -> URL { + return target.directoryURL.appending(path: "bridge-js.config.json") } private func createExportSwiftCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command { let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.swift") let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ExportSwift.json") - let inputFiles = target.sourceFiles.filter { !$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/") } - .map(\.url) + let inputSwiftFiles = target.sourceFiles.filter { + !$0.url.path.hasPrefix(context.pluginWorkDirectoryURL.path + "/") + } + .map(\.url) + let configFile = pathToConfigFile(target: target) + let inputFiles: [URL] + if FileManager.default.fileExists(atPath: configFile.path) { + inputFiles = inputSwiftFiles + [configFile] + } else { + inputFiles = inputSwiftFiles + } return .buildCommand( displayName: "Export Swift API", executable: try context.tool(named: "BridgeJSTool").url, @@ -31,8 +46,10 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { outputSkeletonPath.path, "--output-swift", outputSwiftPath.path, + // Generate the output files even if nothing is exported not to surprise + // the build system. "--always-write", "true", - ] + inputFiles.map(\.path), + ] + inputSwiftFiles.map(\.path), inputFiles: inputFiles, outputFiles: [ outputSwiftPath @@ -40,12 +57,21 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { ) } - private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command { + private func createImportTSCommand(context: PluginContext, target: SwiftSourceModuleTarget) throws -> Command? { let outputSwiftPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.swift") let outputSkeletonPath = context.pluginWorkDirectoryURL.appending(path: "ImportTS.json") - let inputFiles = [ - target.directoryURL.appending(path: "bridge.d.ts") - ] + let inputTSFile = target.directoryURL.appending(path: "bridge-js.d.ts") + guard FileManager.default.fileExists(atPath: inputTSFile.path) else { + return nil + } + + let configFile = pathToConfigFile(target: target) + let inputFiles: [URL] + if FileManager.default.fileExists(atPath: configFile.path) { + inputFiles = [inputTSFile, configFile] + } else { + inputFiles = [inputTSFile] + } return .buildCommand( displayName: "Import TypeScript API", executable: try context.tool(named: "BridgeJSTool").url, @@ -57,10 +83,13 @@ struct BridgeJSBuildPlugin: BuildToolPlugin { outputSwiftPath.path, "--module-name", target.name, + // Generate the output files even if nothing is imported not to surprise + // the build system. "--always-write", "true", "--project", context.package.directoryURL.appending(path: "tsconfig.json").path, - ] + inputFiles.map(\.path), + inputTSFile.path, + ], inputFiles: inputFiles, outputFiles: [ outputSwiftPath diff --git a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift index 286b052d5..f20f78379 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCommandPlugin/BridgeJSCommandPlugin.swift @@ -12,10 +12,12 @@ struct BridgeJSCommandPlugin: CommandPlugin { struct Options { var targets: [String] + var verbose: Bool static func parse(extractor: inout ArgumentExtractor) -> Options { let targets = extractor.extractOption(named: "target") - return Options(targets: targets) + let verbose = extractor.extractFlag(named: "verbose") + return Options(targets: targets, verbose: verbose != 0) } static func help() -> String { @@ -29,13 +31,13 @@ struct BridgeJSCommandPlugin: CommandPlugin { OPTIONS: --target Specify target(s) to generate bridge code for. If omitted, generates for all targets with JavaScriptKit dependency. + --verbose Print verbose output. """ } } func performCommand(context: PluginContext, arguments: [String]) throws { // Check for help flags to display usage information - // This allows users to run `swift package plugin bridge-js --help` to understand the plugin's functionality if arguments.contains(where: { ["-h", "--help"].contains($0) }) { printStderr(Options.help()) return @@ -45,25 +47,31 @@ struct BridgeJSCommandPlugin: CommandPlugin { let options = Options.parse(extractor: &extractor) let remainingArguments = extractor.remainingArguments + let context = Context(options: options, context: context) + if options.targets.isEmpty { - try runOnTargets( - context: context, + try context.runOnTargets( remainingArguments: remainingArguments, where: { target in target.hasDependency(named: Self.JAVASCRIPTKIT_PACKAGE_NAME) } ) } else { - try runOnTargets( - context: context, + try context.runOnTargets( remainingArguments: remainingArguments, where: { options.targets.contains($0.name) } ) } } - private func runOnTargets( - context: PluginContext, + struct Context { + let options: Options + let context: PluginContext + } +} + +extension BridgeJSCommandPlugin.Context { + func runOnTargets( remainingArguments: [String], where predicate: (SwiftSourceModuleTarget) -> Bool ) throws { @@ -71,57 +79,71 @@ struct BridgeJSCommandPlugin: CommandPlugin { guard let target = target as? SwiftSourceModuleTarget else { continue } + let configFilePath = target.directoryURL.appending(path: "bridge-js.config.json") + if !FileManager.default.fileExists(atPath: configFilePath.path) { + printVerbose("No bridge-js.config.json found for \(target.name), skipping...") + continue + } guard predicate(target) else { continue } - try runSingleTarget(context: context, target: target, remainingArguments: remainingArguments) + try runSingleTarget(target: target, remainingArguments: remainingArguments) } } private func runSingleTarget( - context: PluginContext, target: SwiftSourceModuleTarget, remainingArguments: [String] ) throws { - Diagnostics.progress("Exporting Swift API for \(target.name)...") + printStderr("Generating bridge code for \(target.name)...") + + printVerbose("Exporting Swift API for \(target.name)...") let generatedDirectory = target.directoryURL.appending(path: "Generated") let generatedJavaScriptDirectory = generatedDirectory.appending(path: "JavaScript") try runBridgeJSTool( - context: context, arguments: [ "export", "--output-skeleton", generatedJavaScriptDirectory.appending(path: "ExportSwift.json").path, "--output-swift", generatedDirectory.appending(path: "ExportSwift.swift").path, + "--verbose", + options.verbose ? "true" : "false", ] + target.sourceFiles.filter { !$0.url.path.hasPrefix(generatedDirectory.path + "/") }.map(\.url.path) + remainingArguments ) - try runBridgeJSTool( - context: context, - arguments: [ - "import", - "--output-skeleton", - generatedJavaScriptDirectory.appending(path: "ImportTS.json").path, - "--output-swift", - generatedDirectory.appending(path: "ImportTS.swift").path, - "--module-name", - target.name, - "--project", - context.package.directoryURL.appending(path: "tsconfig.json").path, - target.directoryURL.appending(path: "bridge.d.ts").path, - ] + remainingArguments - ) + printVerbose("Importing TypeScript API for \(target.name)...") + + let bridgeDtsPath = target.directoryURL.appending(path: "bridge-js.d.ts") + // Execute import only if bridge-js.d.ts exists + if FileManager.default.fileExists(atPath: bridgeDtsPath.path) { + try runBridgeJSTool( + arguments: [ + "import", + "--output-skeleton", + generatedJavaScriptDirectory.appending(path: "ImportTS.json").path, + "--output-swift", + generatedDirectory.appending(path: "ImportTS.swift").path, + "--verbose", + options.verbose ? "true" : "false", + "--module-name", + target.name, + "--project", + context.package.directoryURL.appending(path: "tsconfig.json").path, + bridgeDtsPath.path, + ] + remainingArguments + ) + } } - private func runBridgeJSTool(context: PluginContext, arguments: [String]) throws { + private func runBridgeJSTool(arguments: [String]) throws { let tool = try context.tool(named: "BridgeJSTool").url - printStderr("$ \(tool.path) \(arguments.joined(separator: " "))") + printVerbose("$ \(tool.path) \(arguments.joined(separator: " "))") let process = Process() process.executableURL = tool process.arguments = arguments @@ -133,6 +155,12 @@ struct BridgeJSCommandPlugin: CommandPlugin { exit(process.terminationStatus) } } + + private func printVerbose(_ message: String) { + if options.verbose { + printStderr(message) + } + } } private func printStderr(_ message: String) { diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index e62a9a639..f9e159844 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -4,6 +4,17 @@ struct BridgeJSLink { /// The exported skeletons var exportedSkeletons: [ExportedSkeleton] = [] var importedSkeletons: [ImportedModuleSkeleton] = [] + let sharedMemory: Bool + + init( + exportedSkeletons: [ExportedSkeleton] = [], + importedSkeletons: [ImportedModuleSkeleton] = [], + sharedMemory: Bool + ) { + self.exportedSkeletons = exportedSkeletons + self.importedSkeletons = importedSkeletons + self.sharedMemory = sharedMemory + } mutating func addExportedSkeletonFile(data: Data) throws { let skeleton = try JSONDecoder().decode(ExportedSkeleton.self, from: data) @@ -47,10 +58,8 @@ struct BridgeJSLink { func link() throws -> (outputJs: String, outputDts: String) { var exportsLines: [String] = [] - var importedLines: [String] = [] var classLines: [String] = [] var dtsExportLines: [String] = [] - var dtsImportLines: [String] = [] var dtsClassLines: [String] = [] if exportedSkeletons.contains(where: { $0.classes.count > 0 }) { @@ -84,57 +93,18 @@ struct BridgeJSLink { } } + var importObjectBuilders: [ImportObjectBuilder] = [] for skeletonSet in importedSkeletons { - importedLines.append("const \(skeletonSet.moduleName) = importObject[\"\(skeletonSet.moduleName)\"] = {};") - func assignToImportObject(name: String, function: [String]) { - var js = function - js[0] = "\(skeletonSet.moduleName)[\"\(name)\"] = " + js[0] - importedLines.append(contentsOf: js) - } + let importObjectBuilder = ImportObjectBuilder(moduleName: skeletonSet.moduleName) for fileSkeleton in skeletonSet.children { for function in fileSkeleton.functions { - let (js, dts) = try renderImportedFunction(function: function) - assignToImportObject(name: function.abiName(context: nil), function: js) - dtsImportLines.append(contentsOf: dts) + try renderImportedFunction(importObjectBuilder: importObjectBuilder, function: function) } for type in fileSkeleton.types { - for property in type.properties { - let getterAbiName = property.getterAbiName(context: type) - let (js, dts) = try renderImportedProperty( - property: property, - abiName: getterAbiName, - emitCall: { thunkBuilder in - thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type) - return try thunkBuilder.lowerReturnValue(returnType: property.type) - } - ) - assignToImportObject(name: getterAbiName, function: js) - dtsImportLines.append(contentsOf: dts) - - if !property.isReadonly { - let setterAbiName = property.setterAbiName(context: type) - let (js, dts) = try renderImportedProperty( - property: property, - abiName: setterAbiName, - emitCall: { thunkBuilder in - thunkBuilder.liftParameter( - param: Parameter(label: nil, name: "newValue", type: property.type) - ) - thunkBuilder.callPropertySetter(name: property.name, returnType: property.type) - return nil - } - ) - assignToImportObject(name: setterAbiName, function: js) - dtsImportLines.append(contentsOf: dts) - } - } - for method in type.methods { - let (js, dts) = try renderImportedMethod(context: type, method: method) - assignToImportObject(name: method.abiName(context: type), function: js) - dtsImportLines.append(contentsOf: dts) - } + try renderImportedType(importObjectBuilder: importObjectBuilder, type: type) } } + importObjectBuilders.append(importObjectBuilder) } let outputJs = """ @@ -152,13 +122,14 @@ struct BridgeJSLink { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { const bjs = {}; importObject["bjs"] = bjs; bjs["return_string"] = function(ptr, len) { - const bytes = new Uint8Array(memory.buffer, ptr, len); + const bytes = new Uint8Array(memory.buffer, ptr, len)\(sharedMemory ? ".slice()" : ""); tmpRetString = textDecoder.decode(bytes); } bjs["init_memory"] = function(sourceId, bytesPtr) { @@ -167,7 +138,7 @@ struct BridgeJSLink { bytes.set(source); } bjs["make_jsstring"] = function(ptr, len) { - const bytes = new Uint8Array(memory.buffer, ptr, len); + const bytes = new Uint8Array(memory.buffer, ptr, len)\(sharedMemory ? ".slice()" : ""); return swift.memory.retain(textDecoder.decode(bytes)); } bjs["init_memory_with_result"] = function(ptr, len) { @@ -175,7 +146,16 @@ struct BridgeJSLink { target.set(tmpRetBytes); tmpRetBytes = undefined; } - \(importedLines.map { $0.indent(count: 12) }.joined(separator: "\n")) + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + \(importObjectBuilders.flatMap { $0.importedLines }.map { $0.indent(count: 12) }.joined(separator: "\n")) }, setInstance: (i) => { instance = i; @@ -198,7 +178,7 @@ struct BridgeJSLink { dtsLines.append(contentsOf: dtsExportLines.map { $0.indent(count: 4) }) dtsLines.append("}") dtsLines.append("export type Imports = {") - dtsLines.append(contentsOf: dtsImportLines.map { $0.indent(count: 4) }) + dtsLines.append(contentsOf: importObjectBuilders.flatMap { $0.dtsImportLines }.map { $0.indent(count: 4) }) dtsLines.append("}") let outputDts = """ // NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, @@ -223,6 +203,11 @@ struct BridgeJSLink { var bodyLines: [String] = [] var cleanupLines: [String] = [] var parameterForwardings: [String] = [] + let effects: Effects + + init(effects: Effects) { + self.effects = effects + } func lowerParameter(param: Parameter) { switch param.type { @@ -280,7 +265,24 @@ struct BridgeJSLink { } func callConstructor(abiName: String) -> String { - return "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))" + let call = "instance.exports.\(abiName)(\(parameterForwardings.joined(separator: ", ")))" + bodyLines.append("const ret = \(call);") + return "ret" + } + + func checkExceptionLines() -> [String] { + guard effects.isThrows else { + return [] + } + return [ + "if (tmpRetException) {", + // TODO: Implement "take" operation + " const error = swift.memory.getObject(tmpRetException);", + " swift.memory.release(tmpRetException);", + " tmpRetException = undefined;", + " throw error;", + "}", + ] } func renderFunction( @@ -296,6 +298,7 @@ struct BridgeJSLink { ) funcLines.append(contentsOf: bodyLines.map { $0.indent(count: 4) }) funcLines.append(contentsOf: cleanupLines.map { $0.indent(count: 4) }) + funcLines.append(contentsOf: checkExceptionLines().map { $0.indent(count: 4) }) if let returnExpr = returnExpr { funcLines.append("return \(returnExpr);".indent(count: 4)) } @@ -309,7 +312,7 @@ struct BridgeJSLink { } func renderExportedFunction(function: ExportedFunction) -> (js: [String], dts: [String]) { - let thunkBuilder = ExportedThunkBuilder() + let thunkBuilder = ExportedThunkBuilder(effects: function.effects) for param in function.parameters { thunkBuilder.lowerParameter(param: param) } @@ -339,16 +342,17 @@ struct BridgeJSLink { jsLines.append("class \(klass.name) extends SwiftHeapObject {") if let constructor: ExportedConstructor = klass.constructor { - let thunkBuilder = ExportedThunkBuilder() + let thunkBuilder = ExportedThunkBuilder(effects: constructor.effects) for param in constructor.parameters { thunkBuilder.lowerParameter(param: param) } - let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) var funcLines: [String] = [] funcLines.append("constructor(\(constructor.parameters.map { $0.name }.joined(separator: ", "))) {") + let returnExpr = thunkBuilder.callConstructor(abiName: constructor.abiName) funcLines.append(contentsOf: thunkBuilder.bodyLines.map { $0.indent(count: 4) }) - funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)) funcLines.append(contentsOf: thunkBuilder.cleanupLines.map { $0.indent(count: 4) }) + funcLines.append(contentsOf: thunkBuilder.checkExceptionLines().map { $0.indent(count: 4) }) + funcLines.append("super(\(returnExpr), instance.exports.bjs_\(klass.name)_deinit);".indent(count: 4)) funcLines.append("}") jsLines.append(contentsOf: funcLines.map { $0.indent(count: 4) }) @@ -359,7 +363,7 @@ struct BridgeJSLink { } for method in klass.methods { - let thunkBuilder = ExportedThunkBuilder() + let thunkBuilder = ExportedThunkBuilder(effects: method.effects) thunkBuilder.lowerSelf() for param in method.parameters { thunkBuilder.lowerParameter(param: param) @@ -437,6 +441,11 @@ struct BridgeJSLink { } } + func callConstructor(name: String) { + let call = "new options.imports.\(name)(\(parameterForwardings.joined(separator: ", ")))" + bodyLines.append("let ret = \(call);") + } + func callMethod(name: String, returnType: BridgeType) { let call = "swift.memory.getObject(self).\(name)(\(parameterForwardings.joined(separator: ", ")))" if returnType == .void { @@ -475,7 +484,31 @@ struct BridgeJSLink { } } - func renderImportedFunction(function: ImportedFunctionSkeleton) throws -> (js: [String], dts: [String]) { + class ImportObjectBuilder { + var moduleName: String + var importedLines: [String] = [] + var dtsImportLines: [String] = [] + + init(moduleName: String) { + self.moduleName = moduleName + importedLines.append("const \(moduleName) = importObject[\"\(moduleName)\"] = {};") + } + + func assignToImportObject(name: String, function: [String]) { + var js = function + js[0] = "\(moduleName)[\"\(name)\"] = " + js[0] + importedLines.append(contentsOf: js) + } + + func appendDts(_ lines: [String]) { + dtsImportLines.append(contentsOf: lines) + } + } + + func renderImportedFunction( + importObjectBuilder: ImportObjectBuilder, + function: ImportedFunctionSkeleton + ) throws { let thunkBuilder = ImportedThunkBuilder() for param in function.parameters { thunkBuilder.liftParameter(param: param) @@ -486,11 +519,85 @@ struct BridgeJSLink { name: function.abiName(context: nil), returnExpr: returnExpr ) - var dtsLines: [String] = [] - dtsLines.append( - "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));" + importObjectBuilder.appendDts( + [ + "\(function.name)\(renderTSSignature(parameters: function.parameters, returnType: function.returnType));" + ] ) - return (funcLines, dtsLines) + importObjectBuilder.assignToImportObject(name: function.abiName(context: nil), function: funcLines) + } + + func renderImportedType( + importObjectBuilder: ImportObjectBuilder, + type: ImportedTypeSkeleton + ) throws { + if let constructor = type.constructor { + try renderImportedConstructor( + importObjectBuilder: importObjectBuilder, + type: type, + constructor: constructor + ) + } + for property in type.properties { + let getterAbiName = property.getterAbiName(context: type) + let (js, dts) = try renderImportedProperty( + property: property, + abiName: getterAbiName, + emitCall: { thunkBuilder in + thunkBuilder.callPropertyGetter(name: property.name, returnType: property.type) + return try thunkBuilder.lowerReturnValue(returnType: property.type) + } + ) + importObjectBuilder.assignToImportObject(name: getterAbiName, function: js) + importObjectBuilder.appendDts(dts) + + if !property.isReadonly { + let setterAbiName = property.setterAbiName(context: type) + let (js, dts) = try renderImportedProperty( + property: property, + abiName: setterAbiName, + emitCall: { thunkBuilder in + thunkBuilder.liftParameter( + param: Parameter(label: nil, name: "newValue", type: property.type) + ) + thunkBuilder.callPropertySetter(name: property.name, returnType: property.type) + return nil + } + ) + importObjectBuilder.assignToImportObject(name: setterAbiName, function: js) + importObjectBuilder.appendDts(dts) + } + } + for method in type.methods { + let (js, dts) = try renderImportedMethod(context: type, method: method) + importObjectBuilder.assignToImportObject(name: method.abiName(context: type), function: js) + importObjectBuilder.appendDts(dts) + } + } + + func renderImportedConstructor( + importObjectBuilder: ImportObjectBuilder, + type: ImportedTypeSkeleton, + constructor: ImportedConstructorSkeleton + ) throws { + let thunkBuilder = ImportedThunkBuilder() + for param in constructor.parameters { + thunkBuilder.liftParameter(param: param) + } + let returnType = BridgeType.jsObject(type.name) + thunkBuilder.callConstructor(name: type.name) + let returnExpr = try thunkBuilder.lowerReturnValue(returnType: returnType) + let abiName = constructor.abiName(context: type) + let funcLines = thunkBuilder.renderFunction( + name: abiName, + returnExpr: returnExpr + ) + importObjectBuilder.assignToImportObject(name: abiName, function: funcLines) + importObjectBuilder.appendDts([ + "\(type.name): {", + "new\(renderTSSignature(parameters: constructor.parameters, returnType: returnType));".indent(count: 4), + "}", + ]) } func renderImportedProperty( @@ -552,8 +659,8 @@ extension BridgeType { return "number" case .bool: return "boolean" - case .jsObject: - return "any" + case .jsObject(let name): + return name ?? "any" case .swiftHeapObject(let name): return name } diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 34492682f..873849f97 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -16,6 +16,11 @@ struct Parameter: Codable { let type: BridgeType } +struct Effects: Codable { + var isAsync: Bool + var isThrows: Bool +} + // MARK: - Exported Skeleton struct ExportedFunction: Codable { @@ -23,6 +28,7 @@ struct ExportedFunction: Codable { var abiName: String var parameters: [Parameter] var returnType: BridgeType + var effects: Effects } struct ExportedClass: Codable { @@ -34,6 +40,7 @@ struct ExportedClass: Codable { struct ExportedConstructor: Codable { var abiName: String var parameters: [Parameter] + var effects: Effects } struct ExportedSkeleton: Codable { diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift index a6bd5ff52..396adcc29 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/BridgeJSTool.swift @@ -57,7 +57,6 @@ import SwiftParser """ ) } - let progress = ProgressReporting() switch subcommand { case "import": let parser = ArgumentParser( @@ -71,6 +70,10 @@ import SwiftParser help: "Always write the output files even if no APIs are imported", required: false ), + "verbose": OptionRule( + help: "Print verbose output", + required: false + ), "output-swift": OptionRule(help: "The output file path for the Swift source code", required: true), "output-skeleton": OptionRule( help: "The output file path for the skeleton of the imported TypeScript APIs", @@ -85,6 +88,7 @@ import SwiftParser let (positionalArguments, _, doubleDashOptions) = try parser.parse( arguments: Array(arguments.dropFirst()) ) + let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true") var importer = ImportTS(progress: progress, moduleName: doubleDashOptions["module-name"]!) for inputFile in positionalArguments { if inputFile.hasSuffix(".json") { @@ -145,11 +149,16 @@ import SwiftParser help: "Always write the output files even if no APIs are exported", required: false ), + "verbose": OptionRule( + help: "Print verbose output", + required: false + ), ] ) let (positionalArguments, _, doubleDashOptions) = try parser.parse( arguments: Array(arguments.dropFirst()) ) + let progress = ProgressReporting(verbose: doubleDashOptions["verbose"] == "true") let exporter = ExportSwift(progress: progress) for inputFile in positionalArguments { let sourceURL = URL(fileURLWithPath: inputFile) @@ -253,7 +262,11 @@ private func printStderr(_ message: String) { struct ProgressReporting { let print: (String) -> Void - init(print: @escaping (String) -> Void = { Swift.print($0) }) { + init(verbose: Bool) { + self.init(print: verbose ? { Swift.print($0) } : { _ in }) + } + + private init(print: @escaping (String) -> Void) { self.print = print } diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift index bef43bbca..47a7a0fa7 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ExportSwift.swift @@ -19,7 +19,7 @@ class ExportSwift { private var exportedClasses: [ExportedClass] = [] private var typeDeclResolver: TypeDeclResolver = TypeDeclResolver() - init(progress: ProgressReporting = ProgressReporting()) { + init(progress: ProgressReporting) { self.progress = progress } @@ -155,14 +155,43 @@ class ExportSwift { abiName = "bjs_\(className)_\(name)" } + guard let effects = collectEffects(signature: node.signature) else { + return nil + } + return ExportedFunction( name: name, abiName: abiName, parameters: parameters, - returnType: returnType + returnType: returnType, + effects: effects ) } + private func collectEffects(signature: FunctionSignatureSyntax) -> Effects? { + let isAsync = signature.effectSpecifiers?.asyncSpecifier != nil + var isThrows = false + if let throwsClause: ThrowsClauseSyntax = signature.effectSpecifiers?.throwsClause { + // Limit the thrown type to JSException for now + guard let thrownType = throwsClause.type else { + diagnose( + node: throwsClause, + message: "Thrown type is not specified, only JSException is supported for now" + ) + return nil + } + guard thrownType.trimmedDescription == "JSException" else { + diagnose( + node: throwsClause, + message: "Only JSException is supported for thrown type, got \(thrownType.trimmedDescription)" + ) + return nil + } + isThrows = true + } + return Effects(isAsync: isAsync, isThrows: isThrows) + } + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { guard node.attributes.hasJSAttribute() else { return .skipChildren } guard case .classBody(let name) = state else { @@ -180,9 +209,14 @@ class ExportSwift { parameters.append(Parameter(label: label, name: name, type: type)) } + guard let effects = collectEffects(signature: node.signature) else { + return .skipChildren + } + let constructor = ExportedConstructor( abiName: "bjs_\(name)_init", - parameters: parameters + parameters: parameters, + effects: effects ) exportedClasses[name]?.constructor = constructor return .skipChildren @@ -221,11 +255,9 @@ class ExportSwift { return nil } guard let typeDecl = typeDeclResolver.lookupType(for: identifier) else { - print("Failed to lookup type \(type.trimmedDescription): not found in typeDeclResolver") return nil } guard typeDecl.is(ClassDeclSyntax.self) || typeDecl.is(ActorDeclSyntax.self) else { - print("Failed to lookup type \(type.trimmedDescription): is not a class or actor") return nil } return .swiftHeapObject(typeDecl.name.text) @@ -237,10 +269,20 @@ class ExportSwift { // // To update this file, just rebuild your project or run // `swift package bridge-js`. + + @_spi(JSObject_id) import JavaScriptKit + + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) + + @_extern(wasm, module: "bjs", name: "swift_js_retain") + private func _swift_js_retain(_ ptr: Int32) -> Int32 + @_extern(wasm, module: "bjs", name: "swift_js_throw") + private func _swift_js_throw(_ id: Int32) + #endif """ func renderSwiftGlue() -> String? { @@ -264,6 +306,11 @@ class ExportSwift { var abiParameterForwardings: [LabeledExprSyntax] = [] var abiParameterSignatures: [(name: String, type: WasmCoreType)] = [] var abiReturnType: WasmCoreType? + let effects: Effects + + init(effects: Effects) { + self.effects = effects + } func liftParameter(param: Parameter) { switch param.type { @@ -317,11 +364,19 @@ class ExportSwift { ) abiParameterSignatures.append((bytesLabel, .i32)) abiParameterSignatures.append((lengthLabel, .i32)) - case .jsObject: + case .jsObject(nil): abiParameterForwardings.append( LabeledExprSyntax( label: param.label, - expression: ExprSyntax("\(raw: param.name)") + expression: ExprSyntax("JSObject(id: UInt32(bitPattern: \(raw: param.name)))") + ) + ) + abiParameterSignatures.append((param.name, .i32)) + case .jsObject(let name): + abiParameterForwardings.append( + LabeledExprSyntax( + label: param.label, + expression: ExprSyntax("\(raw: name)(takingThis: UInt32(bitPattern: \(raw: param.name)))") ) ) abiParameterSignatures.append((param.name, .i32)) @@ -338,35 +393,40 @@ class ExportSwift { } } - func call(name: String, returnType: BridgeType) { + private func renderCallStatement(callee: ExprSyntax, returnType: BridgeType) -> StmtSyntax { + var callExpr: ExprSyntax = + "\(raw: callee)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))" + if effects.isAsync { + callExpr = ExprSyntax(AwaitExprSyntax(awaitKeyword: .keyword(.await), expression: callExpr)) + } + if effects.isThrows { + callExpr = ExprSyntax( + TryExprSyntax( + tryKeyword: .keyword(.try).with(\.trailingTrivia, .space), + expression: callExpr + ) + ) + } let retMutability = returnType == .string ? "var" : "let" - let callExpr: ExprSyntax = - "\(raw: name)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))" if returnType == .void { - body.append("\(raw: callExpr)") + return StmtSyntax("\(raw: callExpr)") } else { - body.append( - """ - \(raw: retMutability) ret = \(raw: callExpr) - """ - ) + return StmtSyntax("\(raw: retMutability) ret = \(raw: callExpr)") } } + func call(name: String, returnType: BridgeType) { + let stmt = renderCallStatement(callee: "\(raw: name)", returnType: returnType) + body.append(CodeBlockItemSyntax(item: .stmt(stmt))) + } + func callMethod(klassName: String, methodName: String, returnType: BridgeType) { let _selfParam = self.abiParameterForwardings.removeFirst() - let retMutability = returnType == .string ? "var" : "let" - let callExpr: ExprSyntax = - "\(raw: _selfParam).\(raw: methodName)(\(raw: abiParameterForwardings.map { $0.description }.joined(separator: ", ")))" - if returnType == .void { - body.append("\(raw: callExpr)") - } else { - body.append( - """ - \(raw: retMutability) ret = \(raw: callExpr) - """ - ) - } + let stmt = renderCallStatement( + callee: "\(raw: _selfParam).\(raw: methodName)", + returnType: returnType + ) + body.append(CodeBlockItemSyntax(item: .stmt(stmt))) } func lowerReturnValue(returnType: BridgeType) { @@ -404,10 +464,16 @@ class ExportSwift { } """ ) - case .jsObject: + case .jsObject(nil): + body.append( + """ + return _swift_js_retain(Int32(bitPattern: ret.id)) + """ + ) + case .jsObject(_?): body.append( """ - return ret.id + return _swift_js_retain(Int32(bitPattern: ret.this.id)) """ ) case .swiftHeapObject: @@ -422,19 +488,58 @@ class ExportSwift { } func render(abiName: String) -> DeclSyntax { + let body: CodeBlockItemListSyntax + if effects.isThrows { + body = """ + do { + \(CodeBlockItemListSyntax(self.body)) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + \(raw: returnPlaceholderStmt()) + } + """ + } else { + body = CodeBlockItemListSyntax(self.body) + } return """ @_expose(wasm, "\(raw: abiName)") @_cdecl("\(raw: abiName)") public func _\(raw: abiName)(\(raw: parameterSignature())) -> \(raw: returnSignature()) { - \(CodeBlockItemListSyntax(body)) + #if arch(wasm32) + \(body) + #else + fatalError("Only available on WebAssembly") + #endif } """ } + private func returnPlaceholderStmt() -> String { + switch abiReturnType { + case .i32: return "return 0" + case .i64: return "return 0" + case .f32: return "return 0.0" + case .f64: return "return 0.0" + case .pointer: return "return UnsafeMutableRawPointer(bitPattern: -1).unsafelyUnwrapped" + case .none: return "return" + } + } + func parameterSignature() -> String { - abiParameterSignatures.map { "\($0.name): \($0.type.swiftType)" }.joined( - separator: ", " - ) + var nameAndType: [(name: String, abiType: String)] = [] + for (name, type) in abiParameterSignatures { + nameAndType.append((name, type.swiftType)) + } + return nameAndType.map { "\($0.name): \($0.abiType)" }.joined(separator: ", ") } func returnSignature() -> String { @@ -443,7 +548,7 @@ class ExportSwift { } func renderSingleExportedFunction(function: ExportedFunction) -> DeclSyntax { - let builder = ExportedThunkBuilder() + let builder = ExportedThunkBuilder(effects: function.effects) for param in function.parameters { builder.liftParameter(param: param) } @@ -502,7 +607,7 @@ class ExportSwift { func renderSingleExportedClass(klass: ExportedClass) -> [DeclSyntax] { var decls: [DeclSyntax] = [] if let constructor = klass.constructor { - let builder = ExportedThunkBuilder() + let builder = ExportedThunkBuilder(effects: constructor.effects) for param in constructor.parameters { builder.liftParameter(param: param) } @@ -511,7 +616,7 @@ class ExportSwift { decls.append(builder.render(abiName: constructor.abiName)) } for method in klass.methods { - let builder = ExportedThunkBuilder() + let builder = ExportedThunkBuilder(effects: method.effects) builder.liftParameter( param: Parameter(label: nil, name: "_self", type: .swiftHeapObject(klass.name)) ) @@ -564,6 +669,10 @@ extension BridgeType { self = .string case "Bool": self = .bool + case "Void": + self = .void + case "JSObject": + self = .jsObject(nil) default: return nil } diff --git a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift index a97550bd1..c06a02509 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSTool/ImportTS.swift @@ -237,33 +237,46 @@ struct ImportTS { preconditionFailure("assignThis can only be called with a jsObject return type") } abiReturnType = .i32 - body.append("self.this = ret") + body.append("self.this = JSObject(id: UInt32(bitPattern: ret))") } func renderImportDecl() -> DeclSyntax { - return DeclSyntax( - FunctionDeclSyntax( - attributes: AttributeListSyntax(itemsBuilder: { - "@_extern(wasm, module: \"\(raw: moduleName)\", name: \"\(raw: abiName)\")" - }).with(\.trailingTrivia, .newline), - name: .identifier(abiName), - signature: FunctionSignatureSyntax( - parameterClause: FunctionParameterClauseSyntax(parametersBuilder: { - for param in abiParameterSignatures { - FunctionParameterSyntax( - firstName: .wildcardToken(), - secondName: .identifier(param.name), - type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType)) - ) - } - }), - returnClause: ReturnClauseSyntax( - arrow: .arrowToken(), - type: IdentifierTypeSyntax(name: .identifier(abiReturnType.map { $0.swiftType } ?? "Void")) - ) + let baseDecl = FunctionDeclSyntax( + funcKeyword: .keyword(.func).with(\.trailingTrivia, .space), + name: .identifier(abiName), + signature: FunctionSignatureSyntax( + parameterClause: FunctionParameterClauseSyntax(parametersBuilder: { + for param in abiParameterSignatures { + FunctionParameterSyntax( + firstName: .wildcardToken().with(\.trailingTrivia, .space), + secondName: .identifier(param.name), + type: IdentifierTypeSyntax(name: .identifier(param.type.swiftType)) + ) + } + }), + returnClause: ReturnClauseSyntax( + arrow: .arrowToken(), + type: IdentifierTypeSyntax(name: .identifier(abiReturnType.map { $0.swiftType } ?? "Void")) ) ) ) + var externDecl = baseDecl + externDecl.attributes = AttributeListSyntax(itemsBuilder: { + "@_extern(wasm, module: \"\(raw: moduleName)\", name: \"\(raw: abiName)\")" + }).with(\.trailingTrivia, .newline) + var stubDecl = baseDecl + stubDecl.body = CodeBlockSyntax { + """ + fatalError("Only available on WebAssembly") + """ + } + return """ + #if arch(wasm32) + \(externDecl) + #else + \(stubDecl) + #endif + """ } func renderThunkDecl(name: String, parameters: [Parameter], returnType: BridgeType) -> DeclSyntax { @@ -328,14 +341,23 @@ struct ImportTS { @_spi(JSObject_id) import JavaScriptKit + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") - private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 + #else + func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + #if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") - private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - - @_extern(wasm, module: "bjs", name: "free_jsobject") - private func _free_jsobject(_ ptr: Int32) -> Void + func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) + #else + func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") + } + #endif """ func renderSwiftThunk( diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/cli.js b/Plugins/BridgeJS/Sources/JavaScript/src/cli.js index 6d2a1ed84..f708082c6 100644 --- a/Plugins/BridgeJS/Sources/JavaScript/src/cli.js +++ b/Plugins/BridgeJS/Sources/JavaScript/src/cli.js @@ -6,7 +6,15 @@ import ts from 'typescript'; import path from 'path'; class DiagnosticEngine { - constructor() { + /** + * @param {string} level + */ + constructor(level) { + const levelInfo = DiagnosticEngine.LEVELS[level]; + if (!levelInfo) { + throw new Error(`Invalid log level: ${level}`); + } + this.minLevel = levelInfo.level; /** @type {ts.FormatDiagnosticsHost} */ this.formattHost = { getCanonicalFileName: (fileName) => fileName, @@ -23,36 +31,36 @@ class DiagnosticEngine { console.log(message); } - /** - * @param {string} message - * @param {ts.Node | undefined} node - */ - info(message, node = undefined) { - this.printLog("info", '\x1b[32m', message, node); - } - - /** - * @param {string} message - * @param {ts.Node | undefined} node - */ - warn(message, node = undefined) { - this.printLog("warning", '\x1b[33m', message, node); - } - - /** - * @param {string} message - */ - error(message) { - this.printLog("error", '\x1b[31m', message); + static LEVELS = { + "verbose": { + color: '\x1b[34m', + level: 0, + }, + "info": { + color: '\x1b[32m', + level: 1, + }, + "warning": { + color: '\x1b[33m', + level: 2, + }, + "error": { + color: '\x1b[31m', + level: 3, + }, } /** - * @param {string} level - * @param {string} color + * @param {keyof typeof DiagnosticEngine.LEVELS} level * @param {string} message * @param {ts.Node | undefined} node */ - printLog(level, color, message, node = undefined) { + print(level, message, node = undefined) { + const levelInfo = DiagnosticEngine.LEVELS[level]; + if (levelInfo.level < this.minLevel) { + return; + } + const color = levelInfo.color; if (node) { const sourceFile = node.getSourceFile(); const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); @@ -85,7 +93,11 @@ export function main(args) { project: { type: 'string', short: 'p', - } + }, + "log-level": { + type: 'string', + default: 'info', + }, }, allowPositionals: true }) @@ -102,9 +114,9 @@ export function main(args) { } const filePath = options.positionals[0]; - const diagnosticEngine = new DiagnosticEngine(); + const diagnosticEngine = new DiagnosticEngine(options.values["log-level"] || "info"); - diagnosticEngine.info(`Processing ${filePath}...`); + diagnosticEngine.print("verbose", `Processing ${filePath}...`); // Create TypeScript program and process declarations const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile); diff --git a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js index e3887b3c1..0f97ea14a 100644 --- a/Plugins/BridgeJS/Sources/JavaScript/src/processor.js +++ b/Plugins/BridgeJS/Sources/JavaScript/src/processor.js @@ -16,8 +16,7 @@ import ts from 'typescript'; /** * @typedef {{ - * warn: (message: string, node?: ts.Node) => void, - * error: (message: string, node?: ts.Node) => void, + * print: (level: "warning" | "error", message: string, node?: ts.Node) => void, * }} DiagnosticEngine */ @@ -97,7 +96,7 @@ export class TypeProcessor { } }); } catch (error) { - this.diagnosticEngine.error(`Error processing ${sourceFile.fileName}: ${error.message}`); + this.diagnosticEngine.print("error", `Error processing ${sourceFile.fileName}: ${error.message}`); } } @@ -239,7 +238,8 @@ export class TypeProcessor { for (const member of node.members) { if (ts.isPropertyDeclaration(member)) { - // TODO + const property = this.visitPropertyDecl(member); + if (property) properties.push(property); } else if (ts.isMethodDeclaration(member)) { const decl = this.visitFunctionLikeDecl(member); if (decl) methods.push(decl); @@ -383,7 +383,7 @@ export class TypeProcessor { const typeName = this.deriveTypeName(type); if (!typeName) { - this.diagnosticEngine.warn(`Unknown non-nominal type: ${typeString}`, node); + this.diagnosticEngine.print("warning", `Unknown non-nominal type: ${typeString}`, node); return { "jsObject": {} }; } this.seenTypes.set(type, node); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift index e052ed427..3e65ca041 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift @@ -55,7 +55,7 @@ import Testing let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let outputSkeletonData = try encoder.encode(outputSkeleton) - var bridgeJSLink = BridgeJSLink() + var bridgeJSLink = BridgeJSLink(sharedMemory: false) try bridgeJSLink.addExportedSkeletonFile(data: outputSkeletonData) try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Export") } @@ -73,7 +73,7 @@ import Testing encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let outputSkeletonData = try encoder.encode(importTS.skeleton) - var bridgeJSLink = BridgeJSLink() + var bridgeJSLink = BridgeJSLink(sharedMemory: false) try bridgeJSLink.addImportedSkeletonFile(data: outputSkeletonData) try snapshot(bridgeJSLink: bridgeJSLink, name: name + ".Import") } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift new file mode 100644 index 000000000..ce8c30fe1 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/Throws.swift @@ -0,0 +1,3 @@ +@JS func throwsSomething() throws(JSException) { + throw JSException(JSError(message: "TestError").jsValue) +} diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts index d10c0138b..074772f24 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/TypeScriptClass.d.ts @@ -1,4 +1,6 @@ export class Greeter { + name: string; + readonly age: number; constructor(name: string); greet(): string; changeName(name: string): void; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js index caad458db..1e9fa9d0e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/ArrayParameter.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkArray"] = function bjs_checkArray(a) { options.imports.checkArray(swift.memory.getObject(a)); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts index 1e7ca6ab1..ffcbcd14f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.d.ts @@ -7,7 +7,7 @@ export type Exports = { } export type Imports = { - returnAnimatable(): any; + returnAnimatable(): Animatable; } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js index 4b3811859..328ff199f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Interface.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_returnAnimatable"] = function bjs_returnAnimatable() { let ret = options.imports.returnAnimatable(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js index 2d9ee4b10..c86f3fea3 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js index 0d871bbb1..584e13085 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveParameters.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_check"] = function bjs_check(a, b) { options.imports.check(a, b); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js index 8a66f0412..d8b29c90c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js index a638f8642..42f805e4f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/PrimitiveReturn.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkNumber"] = function bjs_checkNumber() { let ret = options.imports.checkNumber(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js index c13cd3585..e6dab48d8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js index 6e5d4bdce..844f6f35b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringParameter.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkString"] = function bjs_checkString(a) { const aObject = swift.memory.getObject(a); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js index 0208d8cea..76710fa7c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js index 26e57959a..abf1ea28c 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/StringReturn.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkString"] = function bjs_checkString() { let ret = options.imports.checkString(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js index 971b9d69d..0595b35a6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/SwiftClass.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { @@ -65,8 +75,9 @@ export async function createInstantiator(options, swift) { constructor(name) { const nameBytes = textEncoder.encode(name); const nameId = swift.memory.retain(nameBytes); - super(instance.exports.bjs_Greeter_init(nameId, nameBytes.length), instance.exports.bjs_Greeter_deinit); + const ret = instance.exports.bjs_Greeter_init(nameId, nameBytes.length); swift.memory.release(nameId); + super(ret, instance.exports.bjs_Greeter_deinit); } greet() { instance.exports.bjs_Greeter_greet(this.pointer); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts new file mode 100644 index 000000000..9199ad1ae --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.d.ts @@ -0,0 +1,18 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export type Exports = { + throwsSomething(): void; +} +export type Imports = { +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js new file mode 100644 index 000000000..f15135ffa --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Throws.Export.js @@ -0,0 +1,71 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + return { + /** @param {WebAssembly.Imports} importObject */ + addImports: (importObject) => { + const bjs = {}; + importObject["bjs"] = bjs; + bjs["return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["make_jsstring"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + + return { + throwsSomething: function bjs_throwsSomething() { + instance.exports.bjs_throwsSomething(); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + }, + }; + }, + } +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js index e5909f6cb..39306e28b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeAlias.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_checkSimple"] = function bjs_checkSimple(a) { options.imports.checkSimple(a); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts index 818d57a9d..bcbcf06f8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.d.ts @@ -7,6 +7,9 @@ export type Exports = { } export type Imports = { + Greeter: { + new(name: string): Greeter; + } } export function createInstantiator(options: { imports: Imports; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js index c7ae6a228..1e893f6eb 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/TypeScriptClass.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,7 +36,36 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; + TestModule["bjs_Greeter_init"] = function bjs_Greeter_init(name) { + const nameObject = swift.memory.getObject(name); + swift.memory.release(name); + let ret = new options.imports.Greeter(nameObject); + return swift.memory.retain(ret); + } + TestModule["bjs_Greeter_name_get"] = function bjs_Greeter_name_get(self) { + let ret = swift.memory.getObject(self).name; + tmpRetBytes = textEncoder.encode(ret); + return tmpRetBytes.length; + } + TestModule["bjs_Greeter_name_set"] = function bjs_Greeter_name_set(self, newValue) { + const newValueObject = swift.memory.getObject(newValue); + swift.memory.release(newValue); + swift.memory.getObject(self).name = newValueObject; + } + TestModule["bjs_Greeter_age_get"] = function bjs_Greeter_age_get(self) { + let ret = swift.memory.getObject(self).age; + return ret; + } TestModule["bjs_Greeter_greet"] = function bjs_Greeter_greet(self) { let ret = swift.memory.getObject(self).greet(); tmpRetBytes = textEncoder.encode(ret); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js index a3dae190f..01daf8612 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Export.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } }, setInstance: (i) => { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js index db9312aa6..0fef27b40 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/VoidParameterVoidReturn.Import.js @@ -12,6 +12,7 @@ export async function createInstantiator(options, swift) { let tmpRetString; let tmpRetBytes; + let tmpRetException; return { /** @param {WebAssembly.Imports} importObject */ addImports: (importObject) => { @@ -35,6 +36,15 @@ export async function createInstantiator(options, swift) { target.set(tmpRetBytes); tmpRetBytes = undefined; } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } const TestModule = importObject["TestModule"] = {}; TestModule["bjs_check"] = function bjs_check() { options.imports.check(); diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json index 4b2dafa1b..23fdeab83 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_check", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "check", "parameters" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift index 6df14156d..3c5fd9aab 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveParameters.swift @@ -3,13 +3,27 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check(a: Int32, b: Float32, c: Float64, d: Int32) -> Void { + #if arch(wasm32) check(a: Int(a), b: b, c: c, d: d == 1) + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json index ae672cb5e..f517c68a5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_checkInt", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkInt", "parameters" : [ @@ -17,6 +21,10 @@ }, { "abiName" : "bjs_checkFloat", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkFloat", "parameters" : [ @@ -29,6 +37,10 @@ }, { "abiName" : "bjs_checkDouble", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkDouble", "parameters" : [ @@ -41,6 +53,10 @@ }, { "abiName" : "bjs_checkBool", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkBool", "parameters" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift index a24b2b312..2c35f786f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/PrimitiveReturn.swift @@ -3,35 +3,61 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + @_expose(wasm, "bjs_checkInt") @_cdecl("bjs_checkInt") public func _bjs_checkInt() -> Int32 { + #if arch(wasm32) let ret = checkInt() return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_checkFloat") @_cdecl("bjs_checkFloat") public func _bjs_checkFloat() -> Float32 { + #if arch(wasm32) let ret = checkFloat() return Float32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_checkDouble") @_cdecl("bjs_checkDouble") public func _bjs_checkDouble() -> Float64 { + #if arch(wasm32) let ret = checkDouble() return Float64(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_checkBool") @_cdecl("bjs_checkBool") public func _bjs_checkBool() -> Int32 { + #if arch(wasm32) let ret = checkBool() return Int32(ret ? 1 : 0) + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json index 0fea9735c..a86fb67ef 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_checkString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkString", "parameters" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift index 080f028ef..219782423 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringParameter.swift @@ -3,17 +3,31 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") public func _bjs_checkString(aBytes: Int32, aLen: Int32) -> Void { + #if arch(wasm32) let a = String(unsafeUninitializedCapacity: Int(aLen)) { b in _init_memory(aBytes, b.baseAddress.unsafelyUnwrapped) return Int(aLen) } checkString(a: a) + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json index c773d0d28..b55365724 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_checkString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "checkString", "parameters" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift index bf0be042c..6aa69da23 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/StringReturn.swift @@ -3,16 +3,30 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + @_expose(wasm, "bjs_checkString") @_cdecl("bjs_checkString") public func _bjs_checkString() -> Void { + #if arch(wasm32) var ret = checkString() return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json index 2aff4c931..d37a9254e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.json @@ -3,6 +3,10 @@ { "constructor" : { "abiName" : "bjs_Greeter_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "parameters" : [ { "label" : "name", @@ -18,6 +22,10 @@ "methods" : [ { "abiName" : "bjs_Greeter_greet", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "greet", "parameters" : [ @@ -30,6 +38,10 @@ }, { "abiName" : "bjs_Greeter_changeName", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "changeName", "parameters" : [ { @@ -55,6 +67,10 @@ "functions" : [ { "abiName" : "bjs_takeGreeter", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "takeGreeter", "parameters" : [ { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift index 20fd9c94f..468d7815d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/SwiftClass.swift @@ -3,45 +3,71 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(greeter: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) takeGreeter(greeter: Unmanaged.fromOpaque(greeter).takeUnretainedValue()) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_init") @_cdecl("bjs_Greeter_init") public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } let ret = Greeter(name: name) return Unmanaged.passRetained(ret).toOpaque() + #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) var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().greet() return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_changeName") @_cdecl("bjs_Greeter_changeName") public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } Unmanaged.fromOpaque(_self).takeUnretainedValue().changeName(name: name) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_deinit") diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json new file mode 100644 index 000000000..053632833 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.json @@ -0,0 +1,23 @@ +{ + "classes" : [ + + ], + "functions" : [ + { + "abiName" : "bjs_throwsSomething", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsSomething", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + } + ] +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift new file mode 100644 index 000000000..1fcad7c4b --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/Throws.swift @@ -0,0 +1,43 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "return_string") +private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) +@_extern(wasm, module: "bjs", name: "init_memory") +private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) + +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + +@_expose(wasm, "bjs_throwsSomething") +@_cdecl("bjs_throwsSomething") +public func _bjs_throwsSomething() -> Void { + #if arch(wasm32) + do { + try throwsSomething() + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return + } + #else + fatalError("Only available on WebAssembly") + #endif +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json index f82cdb829..96f875ab2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.json @@ -5,6 +5,10 @@ "functions" : [ { "abiName" : "bjs_check", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "check", "parameters" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift index cf4b76fe9..42a1ddda2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ExportSwiftTests/VoidParameterVoidReturn.swift @@ -3,13 +3,27 @@ // // To update this file, just rebuild your project or run // `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + @_expose(wasm, "bjs_check") @_cdecl("bjs_check") public func _bjs_check() -> Void { + #if arch(wasm32) check() + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift index 1773223b7..b614bd6f8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/ArrayParameter.swift @@ -6,29 +6,56 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkArray(_ a: JSObject) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkArray") func bjs_checkArray(_ a: Int32) -> Void + #else + func bjs_checkArray(_ a: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkArray(Int32(bitPattern: a.id)) } func checkArrayWithLength(_ a: JSObject, _ b: Double) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkArrayWithLength") func bjs_checkArrayWithLength(_ a: Int32, _ b: Float64) -> Void + #else + func bjs_checkArrayWithLength(_ a: Int32, _ b: Float64) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkArrayWithLength(Int32(bitPattern: a.id), b) } func checkArray(_ a: JSObject) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkArray") func bjs_checkArray(_ a: Int32) -> Void + #else + func bjs_checkArray(_ a: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkArray(Int32(bitPattern: a.id)) } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift index c565a2f8a..c64e7433b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/Interface.swift @@ -6,18 +6,33 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func returnAnimatable() -> Animatable { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_returnAnimatable") func bjs_returnAnimatable() -> Int32 + #else + func bjs_returnAnimatable() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_returnAnimatable() return Animatable(takingThis: ret) } @@ -34,15 +49,27 @@ struct Animatable { } func animate(_ keyframes: JSObject, _ options: JSObject) -> JSObject { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Animatable_animate") func bjs_Animatable_animate(_ self: Int32, _ keyframes: Int32, _ options: Int32) -> Int32 + #else + func bjs_Animatable_animate(_ self: Int32, _ keyframes: Int32, _ options: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Animatable_animate(Int32(bitPattern: self.this.id), Int32(bitPattern: keyframes.id), Int32(bitPattern: options.id)) return JSObject(id: UInt32(bitPattern: ret)) } func getAnimations(_ options: JSObject) -> JSObject { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Animatable_getAnimations") func bjs_Animatable_getAnimations(_ self: Int32, _ options: Int32) -> Int32 + #else + func bjs_Animatable_getAnimations(_ self: Int32, _ options: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Animatable_getAnimations(Int32(bitPattern: self.this.id), Int32(bitPattern: options.id)) return JSObject(id: UInt32(bitPattern: ret)) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift index 4ab7f754d..554fd98c8 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveParameters.swift @@ -6,17 +6,32 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func check(_ a: Double, _ b: Bool) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_check") func bjs_check(_ a: Float64, _ b: Int32) -> Void + #else + func bjs_check(_ a: Float64, _ b: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_check(a, Int32(b ? 1 : 0)) } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift index a60c93239..ec9294076 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/PrimitiveReturn.swift @@ -6,25 +6,46 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkNumber() -> Double { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkNumber") func bjs_checkNumber() -> Float64 + #else + func bjs_checkNumber() -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_checkNumber() return Double(ret) } func checkBoolean() -> Bool { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkBoolean") func bjs_checkBoolean() -> Int32 + #else + func bjs_checkBoolean() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_checkBoolean() return ret == 1 } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift index 491978bc0..d5dd74c6d 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringParameter.swift @@ -6,18 +6,33 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkString(_ a: String) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkString") func bjs_checkString(_ a: Int32) -> Void + #else + func bjs_checkString(_ a: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var a = a let aId = a.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) @@ -26,8 +41,14 @@ func checkString(_ a: String) -> Void { } func checkStringWithLength(_ a: String, _ b: Double) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkStringWithLength") func bjs_checkStringWithLength(_ a: Int32, _ b: Float64) -> Void + #else + func bjs_checkStringWithLength(_ a: Int32, _ b: Float64) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var a = a let aId = a.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift index ce32a6433..07fe07223 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/StringReturn.swift @@ -6,18 +6,33 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkString() -> String { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkString") func bjs_checkString() -> Int32 + #else + func bjs_checkString() -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_checkString() return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift index 79f29c925..cfd1d2ec1 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeAlias.swift @@ -6,17 +6,32 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func checkSimple(_ a: Double) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_checkSimple") func bjs_checkSimple(_ a: Float64) -> Void + #else + func bjs_checkSimple(_ a: Float64) -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_checkSimple(a) } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift index 993a14173..7afd45cf2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/TypeScriptClass.swift @@ -6,14 +6,23 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif struct Greeter { let this: JSObject @@ -27,19 +36,79 @@ struct Greeter { } init(_ name: String) { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_init") func bjs_Greeter_init(_ name: Int32) -> Int32 + #else + func bjs_Greeter_init(_ name: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) } let ret = bjs_Greeter_init(nameId) - self.this = ret + self.this = JSObject(id: UInt32(bitPattern: ret)) + } + + var name: String { + get { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_Greeter_name_get") + func bjs_Greeter_name_get(_ self: Int32) -> Int32 + #else + func bjs_Greeter_name_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_Greeter_name_get(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + nonmutating set { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_Greeter_name_set") + func bjs_Greeter_name_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_Greeter_name_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + var newValue = newValue + let newValueId = newValue.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_Greeter_name_set(Int32(bitPattern: self.this.id), newValueId) + } + } + + var age: Double { + get { + #if arch(wasm32) + @_extern(wasm, module: "Check", name: "bjs_Greeter_age_get") + func bjs_Greeter_age_get(_ self: Int32) -> Float64 + #else + func bjs_Greeter_age_get(_ self: Int32) -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_Greeter_age_get(Int32(bitPattern: self.this.id)) + return Double(ret) + } } func greet() -> String { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_greet") func bjs_Greeter_greet(_ self: Int32) -> Int32 + #else + func bjs_Greeter_greet(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif let ret = bjs_Greeter_greet(Int32(bitPattern: self.this.id)) return String(unsafeUninitializedCapacity: Int(ret)) { b in _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) @@ -48,8 +117,14 @@ struct Greeter { } func changeName(_ name: String) -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_Greeter_changeName") func bjs_Greeter_changeName(_ self: Int32, _ name: Int32) -> Void + #else + func bjs_Greeter_changeName(_ self: Int32, _ name: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif var name = name let nameId = name.withUTF8 { b in _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift index 3f2ecc78c..dc384986b 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/ImportTSTests/VoidParameterVoidReturn.swift @@ -6,17 +6,32 @@ @_spi(JSObject_id) import JavaScriptKit +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "make_jsstring") -private func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "init_memory_with_result") -private func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) - -@_extern(wasm, module: "bjs", name: "free_jsobject") -private func _free_jsobject(_ ptr: Int32) -> Void +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif func check() -> Void { + #if arch(wasm32) @_extern(wasm, module: "Check", name: "bjs_check") func bjs_check() -> Void + #else + func bjs_check() -> Void { + fatalError("Only available on WebAssembly") + } + #endif bjs_check() } \ No newline at end of file diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift new file mode 100644 index 000000000..84130401a --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "Check", + dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")], + targets: [ + .testTarget( + name: "CheckTests", + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"), + ], + path: "Tests" + ) + ] +) diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift new file mode 100644 index 000000000..9ed73b7ce --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting/Tests/CheckTests.swift @@ -0,0 +1,5 @@ +import Testing + +@Test func never() async throws { + let _: Void = await withUnsafeContinuation { _ in } +} diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift new file mode 100644 index 000000000..84130401a --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "Check", + dependencies: [.package(name: "JavaScriptKit", path: "../../../../../")], + targets: [ + .testTarget( + name: "CheckTests", + dependencies: [ + "JavaScriptKit", + .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit"), + ], + path: "Tests" + ) + ] +) diff --git a/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift new file mode 100644 index 000000000..324df3701 --- /dev/null +++ b/Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest/Tests/CheckTests.swift @@ -0,0 +1,7 @@ +import XCTest + +final class CheckTests: XCTestCase { + func testNever() async throws { + let _: Void = await withUnsafeContinuation { _ in } + } +} diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 2b8b4458a..48f84e54d 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -569,8 +569,8 @@ struct PackagingPlanner { "BridgeJS is still an experimental feature. Set the environment variable JAVASCRIPTKIT_EXPERIMENTAL_BRIDGEJS=1 to enable." ) } - let bridgeJs = outputDir.appending(path: "bridge.js") - let bridgeDts = outputDir.appending(path: "bridge.d.ts") + let bridgeJs = outputDir.appending(path: "bridge-js.js") + let bridgeDts = outputDir.appending(path: "bridge-js.d.ts") packageInputs.append( make.addTask(inputFiles: exportedSkeletons + importedSkeletons, output: bridgeJs) { _, scope in let link = try BridgeJSLink( @@ -583,7 +583,8 @@ struct PackagingPlanner { let decoder = JSONDecoder() let data = try Data(contentsOf: URL(fileURLWithPath: scope.resolve(path: $0).path)) return try decoder.decode(ImportedModuleSkeleton.self, from: data) - } + }, + sharedMemory: Self.isSharedMemoryEnabled(triple: triple) ) let (outputJs, outputDts) = try link.link() try system.writeFile(atPath: scope.resolve(path: bridgeJs).path, content: Data(outputJs.utf8)) @@ -699,7 +700,7 @@ struct PackagingPlanner { let inputPath = selfPackageDir.appending(path: file) let conditions: [String: Bool] = [ - "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads", + "USE_SHARED_MEMORY": Self.isSharedMemoryEnabled(triple: triple), "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"), "USE_WASI_CDN": options.useCDN, "HAS_BRIDGE": exportedSkeletons.count > 0 || importedSkeletons.count > 0, @@ -742,6 +743,10 @@ struct PackagingPlanner { try system.writeFile(atPath: $1.resolve(path: $0.output).path, content: Data(content.utf8)) } } + + private static func isSharedMemoryEnabled(triple: String) -> Bool { + return triple == "wasm32-unknown-wasip1-threads" + } } // MARK: - Utilities diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index e7f74e974..04f4dcd45 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -71,6 +71,27 @@ struct PackageToJSPlugin: CommandPlugin { See https://book.swiftwasm.org/getting-started/setup.html for more information. """ }), + ( + // In case the SwiftPM target using BridgeJS didn't specify `.enableExperimentalFeature("Extern")` + { build, arguments in + guard + build.logText.contains("@_extern requires '-enable-experimental-feature Extern'") + else { + return nil + } + return """ + The SwiftPM target using BridgeJS didn't specify `.enableExperimentalFeature("Extern")`. + Please add it to the target's `swiftSettings` configuration. + + For example: + ```swift + dependencies: [...], + swiftSettings: [ + .enableExperimentalFeature("Extern"), + ] + ``` + """ + }), ] private func emitHintMessage(_ message: String) { diff --git a/Plugins/PackageToJS/Templates/bin/test.js b/Plugins/PackageToJS/Templates/bin/test.js index f888b9d1c..e7444e901 100644 --- a/Plugins/PackageToJS/Templates/bin/test.js +++ b/Plugins/PackageToJS/Templates/bin/test.js @@ -42,7 +42,13 @@ const harnesses = { let options = await nodePlatform.defaultNodeSetup({ args: testFrameworkArgs, onExit: (code) => { - if (code !== 0) { return } + // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" + if (code !== 0 && code !== 69) { + const stack = new Error().stack + console.error(`Test failed with exit code ${code}`) + console.error(stack) + return + } // Extract the coverage file from the wasm module const filePath = "default.profraw" const destinationPath = args.values["coverage-file"] ?? filePath @@ -52,16 +58,29 @@ const harnesses = { writeFileSync(destinationPath, profraw); } }, - /* #if USE_SHARED_MEMORY */ +/* #if USE_SHARED_MEMORY */ spawnWorker: nodePlatform.createDefaultWorkerFactory(preludeScript) - /* #endif */ +/* #endif */ }) if (preludeScript) { const prelude = await import(preludeScript) if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: true }) + options = await prelude.setupOptions(options, { isMainThread: true }) } } + process.on("beforeExit", () => { + // NOTE: "beforeExit" is fired when the process exits gracefully without calling `process.exit` + // Either XCTest or swift-testing should always call `process.exit` through `proc_exit` even + // if the test succeeds. So exiting gracefully means something went wrong (e.g. withUnsafeContinuation is leaked) + // Therefore, we exit with code 1 to indicate that the test execution failed. + console.error(` + +================================================================================================= +Detected that the test execution ended without a termination signal from the testing framework. +Hint: This typically means that a continuation leak occurred. +=================================================================================================`) + process.exit(1) + }) await instantiate(options) } catch (e) { if (e instanceof WebAssembly.CompileError) { diff --git a/Plugins/PackageToJS/Templates/instantiate.d.ts b/Plugins/PackageToJS/Templates/instantiate.d.ts index 11837aba8..e42e4f2fd 100644 --- a/Plugins/PackageToJS/Templates/instantiate.d.ts +++ b/Plugins/PackageToJS/Templates/instantiate.d.ts @@ -1,8 +1,8 @@ import type { /* #if USE_SHARED_MEMORY */SwiftRuntimeThreadChannel, /* #endif */SwiftRuntime } from "./runtime.js"; /* #if HAS_BRIDGE */ -// @ts-ignore -export type { Imports, Exports } from "./bridge.js"; +export type { Imports, Exports } from "./bridge-js.js"; +import type { Imports, Exports } from "./bridge-js.js"; /* #else */ export type Imports = {} export type Exports = {} @@ -93,12 +93,30 @@ export type InstantiateOptions = { /** * Add imports to the WebAssembly import object * @param imports - The imports to add + * @param context - The context object */ addToCoreImports?: ( imports: WebAssembly.Imports, - getInstance: () => WebAssembly.Instance | null, - getExports: () => Exports | null, + context: { + getInstance: () => WebAssembly.Instance | null, + getExports: () => Exports | null, + _swift: SwiftRuntime, + } ) => void + + /** + * Instrument the WebAssembly instance + * + * @param instance - The instance of the WebAssembly module + * @param context - The context object + * @returns The instrumented instance + */ + instrumentInstance?: ( + instance: WebAssembly.Instance, + context: { + _swift: SwiftRuntime + } + ) => WebAssembly.Instance } /** diff --git a/Plugins/PackageToJS/Templates/instantiate.js b/Plugins/PackageToJS/Templates/instantiate.js index 08351e67e..65996d867 100644 --- a/Plugins/PackageToJS/Templates/instantiate.js +++ b/Plugins/PackageToJS/Templates/instantiate.js @@ -15,7 +15,7 @@ export const MEMORY_TYPE = { /* #if HAS_BRIDGE */ // @ts-ignore -import { createInstantiator } from "./bridge.js" +import { createInstantiator } from "./bridge-js.js" /* #else */ /** * @param {import('./instantiate.d').InstantiateOptions} options @@ -94,7 +94,11 @@ async function _instantiate( /* #endif */ }; instantiator.addImports(importObject); - options.addToCoreImports?.(importObject, () => instance, () => exports); + options.addToCoreImports?.(importObject, { + getInstance: () => instance, + getExports: () => exports, + _swift: swift, + }); let module; let instance; @@ -117,6 +121,7 @@ async function _instantiate( module = await _WebAssembly.compile(moduleSource); instance = await _WebAssembly.instantiate(module, importObject); } + instance = options.instrumentInstance?.(instance, { _swift: swift }) ?? instance; swift.setInstance(instance); instantiator.setInstance(instance); diff --git a/Plugins/PackageToJS/Templates/platforms/node.js b/Plugins/PackageToJS/Templates/platforms/node.js index c45bdf354..aff708be1 100644 --- a/Plugins/PackageToJS/Templates/platforms/node.js +++ b/Plugins/PackageToJS/Templates/platforms/node.js @@ -59,7 +59,7 @@ export function createDefaultWorkerFactory(preludeScript) { if (preludeScript) { const prelude = await import(preludeScript); if (prelude.setupOptions) { - options = prelude.setupOptions(options, { isMainThread: false }) + options = await prelude.setupOptions(options, { isMainThread: false }) } } await instantiateForThread(tid, startArg, { diff --git a/Plugins/PackageToJS/Templates/runtime.d.ts b/Plugins/PackageToJS/Templates/runtime.d.ts index 9613004cc..353db3894 100644 --- a/Plugins/PackageToJS/Templates/runtime.d.ts +++ b/Plugins/PackageToJS/Templates/runtime.d.ts @@ -1,24 +1,15 @@ type ref = number; type pointer = number; -declare class Memory { - readonly rawMemory: WebAssembly.Memory; - private readonly heap; - constructor(exports: WebAssembly.Exports); - retain: (value: any) => number; - getObject: (ref: number) => any; - release: (ref: number) => void; - bytes: () => Uint8Array; - dataView: () => DataView; - writeBytes: (ptr: pointer, bytes: Uint8Array) => void; - readUint32: (ptr: pointer) => number; - readUint64: (ptr: pointer) => bigint; - readInt64: (ptr: pointer) => bigint; - readFloat64: (ptr: pointer) => number; - writeUint32: (ptr: pointer, value: number) => void; - writeUint64: (ptr: pointer, value: bigint) => void; - writeInt64: (ptr: pointer, value: bigint) => void; - writeFloat64: (ptr: pointer, value: number) => void; +declare class JSObjectSpace { + private _heapValueById; + private _heapEntryByValue; + private _heapNextKey; + constructor(); + retain(value: any): number; + retainByRef(ref: ref): number; + release(ref: ref): void; + getObject(ref: ref): any; } /** @@ -95,7 +86,7 @@ type SwiftRuntimeThreadChannel = { }; declare class ITCInterface { private memory; - constructor(memory: Memory); + constructor(memory: JSObjectSpace); send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { object: any; sendingContext: pointer; @@ -181,7 +172,7 @@ type SwiftRuntimeOptions = { }; declare class SwiftRuntime { private _instance; - private _memory; + private readonly memory; private _closureDeallocator; private options; private version; @@ -189,6 +180,9 @@ declare class SwiftRuntime { private textEncoder; /** The thread ID of the current thread. */ private tid; + private getDataView; + private getUint8Array; + private wasmMemory; UnsafeEventLoopYield: typeof UnsafeEventLoopYield; constructor(options?: SwiftRuntimeOptions); setInstance(instance: WebAssembly.Instance): void; @@ -201,7 +195,6 @@ declare class SwiftRuntime { startThread(tid: number, startArg: number): void; private get instance(); private get exports(); - private get memory(); private get closureDeallocator(); private callHostFunction; /** @deprecated Use `wasmImports` instead */ diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs index 71f7f9a30..66a2e0adc 100644 --- a/Plugins/PackageToJS/Templates/runtime.mjs +++ b/Plugins/PackageToJS/Templates/runtime.mjs @@ -21,7 +21,7 @@ function assertNever(x, message) { } const MAIN_THREAD_TID = -1; -const decode = (kind, payload1, payload2, memory) => { +const decode = (kind, payload1, payload2, objectSpace) => { switch (kind) { case 0 /* Kind.Boolean */: switch (payload1) { @@ -37,7 +37,7 @@ const decode = (kind, payload1, payload2, memory) => { case 6 /* Kind.Function */: case 7 /* Kind.Symbol */: case 8 /* Kind.BigInt */: - return memory.getObject(payload1); + return objectSpace.getObject(payload1); case 4 /* Kind.Null */: return null; case 5 /* Kind.Undefined */: @@ -48,21 +48,18 @@ const decode = (kind, payload1, payload2, memory) => { }; // Note: // `decodeValues` assumes that the size of RawJSValue is 16. -const decodeArray = (ptr, length, memory) => { +const decodeArray = (ptr, length, memory, objectSpace) => { // fast path for empty array if (length === 0) { return []; } let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); for (let index = 0; index < length; index++) { const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); + const kind = memory.getUint32(base, true); + const payload1 = memory.getUint32(base + 4, true); + const payload2 = memory.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, objectSpace)); } return result; }; @@ -70,27 +67,27 @@ const decodeArray = (ptr, length, memory) => { // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary // memory stores. // This function should be used only when kind flag is stored in memory. -const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { - const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); - memory.writeUint32(kind_ptr, kind); +const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace); + memory.setUint32(kind_ptr, kind, true); }; -const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { +const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory, objectSpace) => { const exceptionBit = (is_exception ? 1 : 0) << 31; if (value === null) { return exceptionBit | 4 /* Kind.Null */; } const writeRef = (kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); + memory.setUint32(payload1_ptr, objectSpace.retain(value), true); return exceptionBit | kind; }; const type = typeof value; switch (type) { case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); + memory.setUint32(payload1_ptr, value ? 1 : 0, true); return exceptionBit | 0 /* Kind.Boolean */; } case "number": { - memory.writeFloat64(payload2_ptr, value); + memory.setFloat64(payload2_ptr, value, true); return exceptionBit | 2 /* Kind.Number */; } case "string": { @@ -119,84 +116,11 @@ const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, function decodeObjectRefs(ptr, length, memory) { const result = new Array(length); for (let i = 0; i < length; i++) { - result[i] = memory.readUint32(ptr + 4 * i); + result[i] = memory.getUint32(ptr + 4 * i, true); } return result; } -let globalVariable; -if (typeof globalThis !== "undefined") { - globalVariable = globalThis; -} -else if (typeof window !== "undefined") { - globalVariable = window; -} -else if (typeof global !== "undefined") { - globalVariable = global; -} -else if (typeof self !== "undefined") { - globalVariable = self; -} - -class SwiftRuntimeHeap { - constructor() { - this._heapValueById = new Map(); - this._heapValueById.set(0, globalVariable); - this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); - // Note: 0 is preserved for global - this._heapNextKey = 1; - } - retain(value) { - const entry = this._heapEntryByValue.get(value); - if (entry) { - entry.rc++; - return entry.id; - } - const id = this._heapNextKey++; - this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - return id; - } - release(ref) { - const value = this._heapValueById.get(ref); - const entry = this._heapEntryByValue.get(value); - entry.rc--; - if (entry.rc != 0) - return; - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); - } - referenceHeap(ref) { - const value = this._heapValueById.get(ref); - if (value === undefined) { - throw new ReferenceError("Attempted to read invalid reference " + ref); - } - return value; - } -} - -class Memory { - constructor(exports) { - this.heap = new SwiftRuntimeHeap(); - this.retain = (value) => this.heap.retain(value); - this.getObject = (ref) => this.heap.referenceHeap(ref); - this.release = (ref) => this.heap.release(ref); - this.bytes = () => new Uint8Array(this.rawMemory.buffer); - this.dataView = () => new DataView(this.rawMemory.buffer); - this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr); - this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true); - this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true); - this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true); - this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true); - this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true); - this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true); - this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true); - this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true); - this.rawMemory = exports.memory; - } -} - class ITCInterface { constructor(memory) { this.memory = memory; @@ -301,6 +225,61 @@ function deserializeError(error) { return error.value; } +let globalVariable; +if (typeof globalThis !== "undefined") { + globalVariable = globalThis; +} +else if (typeof window !== "undefined") { + globalVariable = window; +} +else if (typeof global !== "undefined") { + globalVariable = global; +} +else if (typeof self !== "undefined") { + globalVariable = self; +} + +class JSObjectSpace { + constructor() { + this._heapValueById = new Map(); + this._heapValueById.set(0, globalVariable); + this._heapEntryByValue = new Map(); + this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); + // Note: 0 is preserved for global + this._heapNextKey = 1; + } + retain(value) { + const entry = this._heapEntryByValue.get(value); + if (entry) { + entry.rc++; + return entry.id; + } + const id = this._heapNextKey++; + this._heapValueById.set(id, value); + this._heapEntryByValue.set(value, { id: id, rc: 1 }); + return id; + } + retainByRef(ref) { + return this.retain(this.getObject(ref)); + } + release(ref) { + const value = this._heapValueById.get(ref); + const entry = this._heapEntryByValue.get(value); + entry.rc--; + if (entry.rc != 0) + return; + this._heapEntryByValue.delete(value); + this._heapValueById.delete(ref); + } + getObject(ref) { + const value = this._heapValueById.get(ref); + if (value === undefined) { + throw new ReferenceError("Attempted to read invalid reference " + ref); + } + return value; + } +} + class SwiftRuntime { constructor(options) { this.version = 708; @@ -310,13 +289,69 @@ class SwiftRuntime { /** @deprecated Use `wasmImports` instead */ this.importObjects = () => this.wasmImports; this._instance = null; - this._memory = null; + this.memory = new JSObjectSpace(); this._closureDeallocator = null; this.tid = null; this.options = options || {}; + this.getDataView = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.getUint8Array = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.wasmMemory = null; } setInstance(instance) { this._instance = instance; + const wasmMemory = instance.exports.memory; + if (wasmMemory instanceof WebAssembly.Memory) { + // Cache the DataView as it's not a cheap operation + let cachedDataView = new DataView(wasmMemory.buffer); + let cachedUint8Array = new Uint8Array(wasmMemory.buffer); + // Check the constructor name of the buffer to determine if it's backed by a SharedArrayBuffer. + // We can't reference SharedArrayBuffer directly here because: + // 1. It may not be available in the global scope if the context is not cross-origin isolated. + // 2. The underlying buffer may be still backed by SAB even if the context is not cross-origin + // isolated (e.g. localhost on Chrome on Android). + if (Object.getPrototypeOf(wasmMemory.buffer).constructor.name === "SharedArrayBuffer") { + // When the wasm memory is backed by a SharedArrayBuffer, growing the memory + // doesn't invalidate the data view by setting the byte length to 0. Instead, + // the data view points to an old buffer after growing the memory. So we have + // to check the buffer identity to determine if the data view is valid. + this.getDataView = () => { + if (cachedDataView.buffer !== wasmMemory.buffer) { + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.buffer !== wasmMemory.buffer) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + else { + this.getDataView = () => { + if (cachedDataView.buffer.byteLength === 0) { + // If the wasm memory is grown, the data view is invalidated, + // so we need to create a new data view. + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.byteLength === 0) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + this.wasmMemory = wasmMemory; + } + else { + throw new Error("instance.exports.memory is not a WebAssembly.Memory!?"); + } if (typeof this.exports._start === "function") { throw new Error(`JavaScriptKit supports only WASI reactor ABI. Please make sure you are building with: @@ -381,12 +416,6 @@ class SwiftRuntime { get exports() { return this.instance.exports; } - get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } get closureDeallocator() { if (this._closureDeallocator) return this._closureDeallocator; @@ -401,10 +430,11 @@ class SwiftRuntime { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); const memory = this.memory; + const dataView = this.getDataView(); for (let index = 0; index < args.length; index++) { const argument = args[index]; const base = argv + 16 * index; - write(argument, base, base + 4, base + 8, false, memory); + write(argument, base, base + 4, base + 8, false, dataView, memory); } let output; // This ref is released by the swjs_call_host_function implementation @@ -483,7 +513,7 @@ class SwiftRuntime { const obj = memory.getObject(ref); const key = memory.getObject(name); const result = obj[key]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_set_subscript: (ref, index, kind, payload1, payload2) => { const memory = this.memory; @@ -494,58 +524,53 @@ class SwiftRuntime { swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { const obj = this.memory.getObject(ref); const result = obj[index]; - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_encode_string: (ref, bytes_ptr_result) => { const memory = this.memory; const bytes = this.textEncoder.encode(memory.getObject(ref)); const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); + this.getDataView().setUint32(bytes_ptr_result, bytes_ptr, true); return bytes.length; }, swjs_decode_string: ( // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer this.options.sharedMemory == true ? ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .slice(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); }) : ((bytes_ptr, length) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); })), swjs_load_string: (ref, buffer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); + const bytes = this.memory.getObject(ref); + this.getUint8Array().set(bytes, buffer); }, swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; const func = memory.getObject(ref); let result = undefined; try { - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = func(...args); } catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.getDataView(), this.memory); } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; const func = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); const result = func(...args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; @@ -553,27 +578,27 @@ class SwiftRuntime { const func = memory.getObject(func_ref); let result; try { - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); } catch (error) { - return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.getDataView(), this.memory); } - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { const memory = this.memory; const obj = memory.getObject(obj_ref); const func = memory.getObject(func_ref); let result = undefined; - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); - return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.getDataView(), this.memory); }, swjs_call_new: (ref, argv, argc) => { const memory = this.memory; const constructor = memory.getObject(ref); - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -582,15 +607,15 @@ class SwiftRuntime { const constructor = memory.getObject(ref); let result; try { - const args = decodeArray(argv, argc, memory); + const args = decodeArray(argv, argc, this.getDataView(), memory); result = new constructor(...args); } catch (error) { - write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.getDataView(), this.memory); return -1; } memory = this.memory; - write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, this.getDataView(), memory); return memory.retain(result); }, swjs_instanceof: (obj_ref, constructor_ref) => { @@ -624,7 +649,7 @@ class SwiftRuntime { // See https://github.com/swiftwasm/swift/issues/5599 return this.memory.retain(new ArrayType()); } - const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + const array = new ArrayType(this.wasmMemory.buffer, elementsPtr, length); // Call `.slice()` to copy the memory return this.memory.retain(array.slice()); }, @@ -633,7 +658,7 @@ class SwiftRuntime { const memory = this.memory; const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); + this.getUint8Array().set(bytes, buffer); }, swjs_release: (ref) => { this.memory.release(ref); @@ -756,8 +781,7 @@ class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); broker.request({ type: "request", data: { @@ -777,9 +801,9 @@ class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const dataView = this.getDataView(); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, dataView); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, dataView); broker.request({ type: "request", data: { diff --git a/Plugins/PackageToJS/Templates/test.d.ts b/Plugins/PackageToJS/Templates/test.d.ts index 2968f6dd9..21383997b 100644 --- a/Plugins/PackageToJS/Templates/test.d.ts +++ b/Plugins/PackageToJS/Templates/test.d.ts @@ -1,5 +1,12 @@ import type { InstantiateOptions, instantiate } from "./instantiate"; +export type SetupOptionsFn = ( + options: InstantiateOptions, + context: { + isMainThread: boolean, + } +) => Promise + export function testBrowser( options: { preludeScript?: string, diff --git a/Plugins/PackageToJS/Templates/test.js b/Plugins/PackageToJS/Templates/test.js index 8c4432492..518dacf20 100644 --- a/Plugins/PackageToJS/Templates/test.js +++ b/Plugins/PackageToJS/Templates/test.js @@ -157,6 +157,7 @@ export async function testBrowserInPage(options, processInfo) { }); const { instantiate } = await import("./instantiate.js"); + /** @type {import('./test.d.ts').SetupOptionsFn} */ let setupOptions = (options, _) => { return options }; if (processInfo.preludeScript) { const prelude = await import(processInfo.preludeScript); @@ -171,8 +172,8 @@ export async function testBrowserInPage(options, processInfo) { // Instantiate the WebAssembly file return await instantiate({ ...options, - addToCoreImports: (imports) => { - options.addToCoreImports?.(imports); + addToCoreImports: (imports, context) => { + options.addToCoreImports?.(imports, context); imports["wasi_snapshot_preview1"]["proc_exit"] = (code) => { exitTest(code); throw new ExitError(code); diff --git a/Plugins/PackageToJS/Tests/ExampleTests.swift b/Plugins/PackageToJS/Tests/ExampleTests.swift index ab0d1d798..d860a685f 100644 --- a/Plugins/PackageToJS/Tests/ExampleTests.swift +++ b/Plugins/PackageToJS/Tests/ExampleTests.swift @@ -88,7 +88,6 @@ extension Trait where Self == ConditionTrait { atPath: destinationPath.path, withDestinationPath: linkDestination ) - enumerator.skipDescendants() continue } @@ -117,8 +116,11 @@ extension Trait where Self == ConditionTrait { typealias RunProcess = (_ executableURL: URL, _ args: [String], _ env: [String: String]) throws -> Void typealias RunSwift = (_ args: [String], _ env: [String: String]) throws -> Void - func withPackage(at path: String, body: (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void) throws - { + func withPackage( + at path: String, + assertTerminationStatus: (Int32) -> Bool = { $0 == 0 }, + body: @escaping (URL, _ runProcess: RunProcess, _ runSwift: RunSwift) throws -> Void + ) throws { try withTemporaryDirectory { tempDir, retain in let destination = tempDir.appending(path: Self.repoPath.lastPathComponent) try Self.copyRepository(to: destination) @@ -139,11 +141,11 @@ extension Trait where Self == ConditionTrait { try process.run() process.waitUntilExit() - if process.terminationStatus != 0 { + if !assertTerminationStatus(process.terminationStatus) { retain = true } try #require( - process.terminationStatus == 0, + assertTerminationStatus(process.terminationStatus), """ Swift package should build successfully, check \(destination.appending(path: path).path) for details stdout: \(stdoutPath.path) @@ -275,4 +277,29 @@ extension Trait where Self == ConditionTrait { ) } } + + @Test(.requireSwiftSDK) + func continuationLeakInTest_XCTest() throws { + let swiftSDKID = try #require(Self.getSwiftSDKID()) + try withPackage( + at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/XCTest", + assertTerminationStatus: { $0 != 0 } + ) { packageDir, _, runSwift in + try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:]) + } + } + + #if compiler(>=6.1) + // TODO: Remove triple restriction once swift-testing is shipped in p1-threads SDK + @Test(.requireSwiftSDK(triple: "wasm32-unknown-wasi")) + func continuationLeakInTest_SwiftTesting() throws { + let swiftSDKID = try #require(Self.getSwiftSDKID()) + try withPackage( + at: "Plugins/PackageToJS/Fixtures/ContinuationLeakInTest/SwiftTesting", + assertTerminationStatus: { $0 != 0 } + ) { packageDir, _, runSwift in + try runSwift(["package", "--disable-sandbox", "--swift-sdk", swiftSDKID, "js", "test"], [:]) + } + } + #endif } diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index a747dec1f..199db33d6 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -7,9 +7,9 @@ import { MAIN_THREAD_TID, } from "./types.js"; import * as JSValue from "./js-value.js"; -import { Memory } from "./memory.js"; import { deserializeError, MainToWorkerMessage, MessageBroker, ResponseMessage, ITCInterface, serializeError, SwiftRuntimeThreadChannel, WorkerToMainMessage } from "./itc.js"; import { decodeObjectRefs } from "./js-value.js"; +import { JSObjectSpace } from "./object-heap.js"; export { SwiftRuntimeThreadChannel }; export type SwiftRuntimeOptions = { @@ -27,7 +27,7 @@ export type SwiftRuntimeOptions = { export class SwiftRuntime { private _instance: WebAssembly.Instance | null; - private _memory: Memory | null; + private readonly memory: JSObjectSpace; private _closureDeallocator: SwiftClosureDeallocator | null; private options: SwiftRuntimeOptions; private version: number = 708; @@ -36,19 +36,77 @@ export class SwiftRuntime { private textEncoder = new TextEncoder(); // Only support utf-8 /** The thread ID of the current thread. */ private tid: number | null; + private getDataView: (() => DataView); + private getUint8Array: (() => Uint8Array); + private wasmMemory: WebAssembly.Memory | null; UnsafeEventLoopYield = UnsafeEventLoopYield; constructor(options?: SwiftRuntimeOptions) { this._instance = null; - this._memory = null; + this.memory = new JSObjectSpace(); this._closureDeallocator = null; this.tid = null; this.options = options || {}; + this.getDataView = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.getUint8Array = () => { + throw new Error("Please call setInstance() before using any JavaScriptKit APIs from Swift."); + }; + this.wasmMemory = null; } setInstance(instance: WebAssembly.Instance) { this._instance = instance; + const wasmMemory = instance.exports.memory; + if (wasmMemory instanceof WebAssembly.Memory) { + // Cache the DataView as it's not a cheap operation + let cachedDataView = new DataView(wasmMemory.buffer); + let cachedUint8Array = new Uint8Array(wasmMemory.buffer); + + // Check the constructor name of the buffer to determine if it's backed by a SharedArrayBuffer. + // We can't reference SharedArrayBuffer directly here because: + // 1. It may not be available in the global scope if the context is not cross-origin isolated. + // 2. The underlying buffer may be still backed by SAB even if the context is not cross-origin + // isolated (e.g. localhost on Chrome on Android). + if (Object.getPrototypeOf(wasmMemory.buffer).constructor.name === "SharedArrayBuffer") { + // When the wasm memory is backed by a SharedArrayBuffer, growing the memory + // doesn't invalidate the data view by setting the byte length to 0. Instead, + // the data view points to an old buffer after growing the memory. So we have + // to check the buffer identity to determine if the data view is valid. + this.getDataView = () => { + if (cachedDataView.buffer !== wasmMemory.buffer) { + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.buffer !== wasmMemory.buffer) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } else { + this.getDataView = () => { + if (cachedDataView.buffer.byteLength === 0) { + // If the wasm memory is grown, the data view is invalidated, + // so we need to create a new data view. + cachedDataView = new DataView(wasmMemory.buffer); + } + return cachedDataView; + }; + this.getUint8Array = () => { + if (cachedUint8Array.byteLength === 0) { + cachedUint8Array = new Uint8Array(wasmMemory.buffer); + } + return cachedUint8Array; + }; + } + this.wasmMemory = wasmMemory; + } else { + throw new Error("instance.exports.memory is not a WebAssembly.Memory!?"); + } if (typeof (this.exports as any)._start === "function") { throw new Error( `JavaScriptKit supports only WASI reactor ABI. @@ -124,13 +182,6 @@ export class SwiftRuntime { return this.instance.exports as any as ExportedFunctions; } - private get memory() { - if (!this._memory) { - this._memory = new Memory(this.instance.exports); - } - return this._memory; - } - private get closureDeallocator(): SwiftClosureDeallocator | null { if (this._closureDeallocator) return this._closureDeallocator; @@ -154,10 +205,11 @@ export class SwiftRuntime { const argc = args.length; const argv = this.exports.swjs_prepare_host_function_call(argc); const memory = this.memory; + const dataView = this.getDataView(); for (let index = 0; index < args.length; index++) { const argument = args[index]; const base = argv + 16 * index; - JSValue.write(argument, base, base + 4, base + 8, false, memory); + JSValue.write(argument, base, base + 4, base + 8, false, dataView, memory); } let output: any; // This ref is released by the swjs_call_host_function implementation @@ -258,7 +310,8 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, - memory + this.getDataView(), + this.memory ); }, @@ -287,6 +340,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -295,33 +349,28 @@ export class SwiftRuntime { const memory = this.memory; const bytes = this.textEncoder.encode(memory.getObject(ref)); const bytes_ptr = memory.retain(bytes); - memory.writeUint32(bytes_ptr_result, bytes_ptr); + this.getDataView().setUint32(bytes_ptr_result, bytes_ptr, true); return bytes.length; }, swjs_decode_string: ( // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer this.options.sharedMemory == true ? ((bytes_ptr: pointer, length: number) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .slice(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); }) : ((bytes_ptr: pointer, length: number) => { - const memory = this.memory; - const bytes = memory - .bytes() + const bytes = this.getUint8Array() .subarray(bytes_ptr, bytes_ptr + length); const string = this.textDecoder.decode(bytes); - return memory.retain(string); + return this.memory.retain(string); }) ), swjs_load_string: (ref: ref, buffer: pointer) => { - const memory = this.memory; - const bytes = memory.getObject(ref); - memory.writeBytes(buffer, bytes); + const bytes = this.memory.getObject(ref); + this.getUint8Array().set(bytes, buffer); }, swjs_call_function: ( @@ -335,7 +384,7 @@ export class SwiftRuntime { const func = memory.getObject(ref); let result = undefined; try { - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = func(...args); } catch (error) { return JSValue.writeAndReturnKindBits( @@ -343,6 +392,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, true, + this.getDataView(), this.memory ); } @@ -351,6 +401,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -363,13 +414,14 @@ export class SwiftRuntime { ) => { const memory = this.memory; const func = memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); const result = func(...args); return JSValue.writeAndReturnKindBits( result, payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -387,7 +439,7 @@ export class SwiftRuntime { const func = memory.getObject(func_ref); let result: any; try { - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); } catch (error) { return JSValue.writeAndReturnKindBits( @@ -395,6 +447,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, true, + this.getDataView(), this.memory ); } @@ -403,6 +456,7 @@ export class SwiftRuntime { payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -418,13 +472,14 @@ export class SwiftRuntime { const obj = memory.getObject(obj_ref); const func = memory.getObject(func_ref); let result = undefined; - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = func.apply(obj, args); return JSValue.writeAndReturnKindBits( result, payload1_ptr, payload2_ptr, false, + this.getDataView(), this.memory ); }, @@ -432,7 +487,7 @@ export class SwiftRuntime { swjs_call_new: (ref: ref, argv: pointer, argc: number) => { const memory = this.memory; const constructor = memory.getObject(ref); - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -448,7 +503,7 @@ export class SwiftRuntime { const constructor = memory.getObject(ref); let result: any; try { - const args = JSValue.decodeArray(argv, argc, memory); + const args = JSValue.decodeArray(argv, argc, this.getDataView(), memory); result = new constructor(...args); } catch (error) { JSValue.write( @@ -457,6 +512,7 @@ export class SwiftRuntime { exception_payload1_ptr, exception_payload2_ptr, true, + this.getDataView(), this.memory ); return -1; @@ -468,6 +524,7 @@ export class SwiftRuntime { exception_payload1_ptr, exception_payload2_ptr, false, + this.getDataView(), memory ); return memory.retain(result); @@ -521,7 +578,7 @@ export class SwiftRuntime { return this.memory.retain(new ArrayType()); } const array = new ArrayType( - this.memory.rawMemory.buffer, + this.wasmMemory!.buffer, elementsPtr, length ); @@ -535,7 +592,7 @@ export class SwiftRuntime { const memory = this.memory; const typedArray = memory.getObject(ref); const bytes = new Uint8Array(typedArray.buffer); - memory.writeBytes(buffer, bytes); + this.getUint8Array().set(bytes, buffer); }, swjs_release: (ref: ref) => { @@ -674,8 +731,7 @@ export class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, this.getDataView()); broker.request({ type: "request", data: { @@ -701,9 +757,9 @@ export class SwiftRuntime { throw new Error("threadChannel is not set in options given to SwiftRuntime. Please set it to request transferring objects."); } const broker = getMessageBroker(this.options.threadChannel); - const memory = this.memory; - const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, memory); - const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, memory); + const dataView = this.getDataView(); + const sendingObjects = decodeObjectRefs(sending_objects, sending_objects_count, dataView); + const transferringObjects = decodeObjectRefs(transferring_objects, transferring_objects_count, dataView); broker.request({ type: "request", data: { diff --git a/Runtime/src/itc.ts b/Runtime/src/itc.ts index e2c93622a..08b420640 100644 --- a/Runtime/src/itc.ts +++ b/Runtime/src/itc.ts @@ -1,6 +1,6 @@ // This file defines the interface for the inter-thread communication. import type { ref, pointer } from "./types.js"; -import { Memory } from "./memory.js"; +import { JSObjectSpace as JSObjectSpace } from "./object-heap.js"; /** * A thread channel is a set of functions that are used to communicate between @@ -83,7 +83,7 @@ export type SwiftRuntimeThreadChannel = export class ITCInterface { - constructor(private memory: Memory) {} + constructor(private memory: JSObjectSpace) {} send(sendingObject: ref, transferringObjects: ref[], sendingContext: pointer): { object: any, sendingContext: pointer, transfer: Transferable[] } { const object = this.memory.getObject(sendingObject); diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index dcc378f61..b23f39d87 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -1,4 +1,4 @@ -import { Memory } from "./memory.js"; +import { JSObjectSpace } from "./object-heap.js"; import { assertNever, JavaScriptValueKindAndFlags, pointer, ref } from "./types.js"; export const enum Kind { @@ -17,7 +17,7 @@ export const decode = ( kind: Kind, payload1: number, payload2: number, - memory: Memory + objectSpace: JSObjectSpace ) => { switch (kind) { case Kind.Boolean: @@ -35,7 +35,7 @@ export const decode = ( case Kind.Function: case Kind.Symbol: case Kind.BigInt: - return memory.getObject(payload1); + return objectSpace.getObject(payload1); case Kind.Null: return null; @@ -50,22 +50,19 @@ export const decode = ( // Note: // `decodeValues` assumes that the size of RawJSValue is 16. -export const decodeArray = (ptr: pointer, length: number, memory: Memory) => { +export const decodeArray = (ptr: pointer, length: number, memory: DataView, objectSpace: JSObjectSpace) => { // fast path for empty array if (length === 0) { return []; } let result = []; - // It's safe to hold DataView here because WebAssembly.Memory.buffer won't - // change within this function. - const view = memory.dataView(); for (let index = 0; index < length; index++) { const base = ptr + 16 * index; - const kind = view.getUint32(base, true); - const payload1 = view.getUint32(base + 4, true); - const payload2 = view.getFloat64(base + 8, true); - result.push(decode(kind, payload1, payload2, memory)); + const kind = memory.getUint32(base, true); + const payload1 = memory.getUint32(base + 4, true); + const payload2 = memory.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, objectSpace)); } return result; }; @@ -80,16 +77,18 @@ export const write = ( payload1_ptr: pointer, payload2_ptr: pointer, is_exception: boolean, - memory: Memory + memory: DataView, + objectSpace: JSObjectSpace ) => { const kind = writeAndReturnKindBits( value, payload1_ptr, payload2_ptr, is_exception, - memory + memory, + objectSpace ); - memory.writeUint32(kind_ptr, kind); + memory.setUint32(kind_ptr, kind, true); }; export const writeAndReturnKindBits = ( @@ -97,7 +96,8 @@ export const writeAndReturnKindBits = ( payload1_ptr: pointer, payload2_ptr: pointer, is_exception: boolean, - memory: Memory + memory: DataView, + objectSpace: JSObjectSpace ): JavaScriptValueKindAndFlags => { const exceptionBit = (is_exception ? 1 : 0) << 31; if (value === null) { @@ -105,18 +105,18 @@ export const writeAndReturnKindBits = ( } const writeRef = (kind: Kind) => { - memory.writeUint32(payload1_ptr, memory.retain(value)); + memory.setUint32(payload1_ptr, objectSpace.retain(value), true); return exceptionBit | kind; }; const type = typeof value; switch (type) { case "boolean": { - memory.writeUint32(payload1_ptr, value ? 1 : 0); + memory.setUint32(payload1_ptr, value ? 1 : 0, true); return exceptionBit | Kind.Boolean; } case "number": { - memory.writeFloat64(payload2_ptr, value); + memory.setFloat64(payload2_ptr, value, true); return exceptionBit | Kind.Number; } case "string": { @@ -143,10 +143,10 @@ export const writeAndReturnKindBits = ( throw new Error("Unreachable"); }; -export function decodeObjectRefs(ptr: pointer, length: number, memory: Memory): ref[] { +export function decodeObjectRefs(ptr: pointer, length: number, memory: DataView): ref[] { const result: ref[] = new Array(length); for (let i = 0; i < length; i++) { - result[i] = memory.readUint32(ptr + 4 * i); + result[i] = memory.getUint32(ptr + 4 * i, true); } return result; } diff --git a/Runtime/src/memory.ts b/Runtime/src/memory.ts deleted file mode 100644 index d8334516d..000000000 --- a/Runtime/src/memory.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SwiftRuntimeHeap } from "./object-heap.js"; -import { pointer } from "./types.js"; - -export class Memory { - readonly rawMemory: WebAssembly.Memory; - - private readonly heap = new SwiftRuntimeHeap(); - - constructor(exports: WebAssembly.Exports) { - this.rawMemory = exports.memory as WebAssembly.Memory; - } - - retain = (value: any) => this.heap.retain(value); - getObject = (ref: number) => this.heap.referenceHeap(ref); - release = (ref: number) => this.heap.release(ref); - - bytes = () => new Uint8Array(this.rawMemory.buffer); - dataView = () => new DataView(this.rawMemory.buffer); - - writeBytes = (ptr: pointer, bytes: Uint8Array) => - this.bytes().set(bytes, ptr); - - readUint32 = (ptr: pointer) => this.dataView().getUint32(ptr, true); - readUint64 = (ptr: pointer) => this.dataView().getBigUint64(ptr, true); - readInt64 = (ptr: pointer) => this.dataView().getBigInt64(ptr, true); - readFloat64 = (ptr: pointer) => this.dataView().getFloat64(ptr, true); - - writeUint32 = (ptr: pointer, value: number) => - this.dataView().setUint32(ptr, value, true); - writeUint64 = (ptr: pointer, value: bigint) => - this.dataView().setBigUint64(ptr, value, true); - writeInt64 = (ptr: pointer, value: bigint) => - this.dataView().setBigInt64(ptr, value, true); - writeFloat64 = (ptr: pointer, value: number) => - this.dataView().setFloat64(ptr, value, true); -} diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index d59f5101e..ecc2d218b 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -5,7 +5,7 @@ type SwiftRuntimeHeapEntry = { id: number; rc: number; }; -export class SwiftRuntimeHeap { +export class JSObjectSpace { private _heapValueById: Map; private _heapEntryByValue: Map; private _heapNextKey: number; @@ -33,6 +33,10 @@ export class SwiftRuntimeHeap { return id; } + retainByRef(ref: ref) { + return this.retain(this.getObject(ref)); + } + release(ref: ref) { const value = this._heapValueById.get(ref); const entry = this._heapEntryByValue.get(value)!; @@ -43,7 +47,7 @@ export class SwiftRuntimeHeap { this._heapValueById.delete(ref); } - referenceHeap(ref: ref) { + getObject(ref: ref) { const value = this._heapValueById.get(ref); if (value === undefined) { throw new ReferenceError( diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift new file mode 100644 index 000000000..ed60eae76 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+ExecutorFactory.swift @@ -0,0 +1,92 @@ +// Implementation of custom executors for JavaScript event loop +// This file implements the ExecutorFactory protocol to provide custom main and global executors +// for Swift concurrency in JavaScript environment. +// See: https://github.com/swiftlang/swift/pull/80266 +// See: https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437 + +import _Concurrency +import _CJavaScriptKit + +#if compiler(>=6.2) + +// MARK: - MainExecutor Implementation +// MainExecutor is used by the main actor to execute tasks on the main thread +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: MainExecutor { + public func run() throws { + // This method is called from `swift_task_asyncMainDrainQueueImpl`. + // https://github.com/swiftlang/swift/blob/swift-DEVELOPMENT-SNAPSHOT-2025-04-12-a/stdlib/public/Concurrency/ExecutorImpl.swift#L28 + // Yield control to the JavaScript event loop to skip the `exit(0)` + // call by `swift_task_asyncMainDrainQueueImpl`. + swjs_unsafe_event_loop_yield() + } + public func stop() {} +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension JavaScriptEventLoop: TaskExecutor {} + +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: SchedulableExecutor { + public func enqueue( + _ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C + ) { + let milliseconds = Self.delayInMilliseconds(from: delay, clock: clock) + self.enqueue( + UnownedJob(job), + withDelay: milliseconds + ) + } + + private static func delayInMilliseconds(from duration: C.Duration, clock: C) -> Double { + let swiftDuration = clock.convert(from: duration)! + let (seconds, attoseconds) = swiftDuration.components + return Double(seconds) * 1_000 + (Double(attoseconds) / 1_000_000_000_000_000) + } +} + +// MARK: - ExecutorFactory Implementation +@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) +extension JavaScriptEventLoop: ExecutorFactory { + // Forward all operations to the current thread's JavaScriptEventLoop instance + final class CurrentThread: TaskExecutor, SchedulableExecutor, MainExecutor, SerialExecutor { + func checkIsolated() {} + + func enqueue(_ job: consuming ExecutorJob) { + JavaScriptEventLoop.shared.enqueue(job) + } + + func enqueue( + _ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C + ) { + JavaScriptEventLoop.shared.enqueue( + job, + after: delay, + tolerance: tolerance, + clock: clock + ) + } + func run() throws { + try JavaScriptEventLoop.shared.run() + } + func stop() { + JavaScriptEventLoop.shared.stop() + } + } + + public static var mainExecutor: any MainExecutor { + CurrentThread() + } + + public static var defaultExecutor: any TaskExecutor { + CurrentThread() + } +} + +#endif // compiler(>=6.2) diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift new file mode 100644 index 000000000..bcab9a3d1 --- /dev/null +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop+LegacyHooks.swift @@ -0,0 +1,107 @@ +import _Concurrency +import _CJavaScriptEventLoop +import _CJavaScriptKit + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + + static func installByLegacyHook() { + #if compiler(>=5.9) + typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( + swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override + ) -> Void + let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in + swjs_unsafe_event_loop_yield() + } + swift_task_asyncMainDrainQueue_hook = unsafeBitCast( + swift_task_asyncMainDrainQueue_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + #endif + + typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) + -> Void + let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in + JavaScriptEventLoop.shared.unsafeEnqueue(job) + } + swift_task_enqueueGlobal_hook = unsafeBitCast( + swift_task_enqueueGlobal_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) ( + UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original + ) -> Void + let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { + nanoseconds, + job, + original in + let milliseconds = Double(nanoseconds / 1_000_000) + JavaScriptEventLoop.shared.enqueue(job, withDelay: milliseconds) + } + swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( + swift_task_enqueueGlobalWithDelay_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + #if compiler(>=5.7) + typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( + Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original + ) -> Void + let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { + sec, + nsec, + tsec, + tnsec, + clock, + job, + original in + JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) + } + swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast( + swift_task_enqueueGlobalWithDeadline_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + #endif + + typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( + UnownedJob, swift_task_enqueueMainExecutor_original + ) -> Void + let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in + JavaScriptEventLoop.shared.unsafeEnqueue(job) + } + swift_task_enqueueMainExecutor_hook = unsafeBitCast( + swift_task_enqueueMainExecutor_hook_impl, + to: UnsafeMutableRawPointer?.self + ) + + } +} + +#if compiler(>=5.7) +/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 +@_silgen_name("swift_get_time") +internal func swift_get_time( + _ seconds: UnsafeMutablePointer, + _ nanoseconds: UnsafeMutablePointer, + _ clock: CInt +) + +@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) +extension JavaScriptEventLoop { + fileprivate func enqueue( + _ job: UnownedJob, + withDelay seconds: Int64, + _ nanoseconds: Int64, + _ toleranceSec: Int64, + _ toleranceNSec: Int64, + _ clock: Int32 + ) { + var nowSec: Int64 = 0 + var nowNSec: Int64 = 0 + swift_get_time(&nowSec, &nowNSec, clock) + let delayMilliseconds = (seconds - nowSec) * 1_000 + (nanoseconds - nowNSec) / 1_000_000 + enqueue(job, withDelay: delayMilliseconds <= 0 ? 0 : Double(delayMilliseconds)) + } +} +#endif diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift index 8948723d4..1cb90f8d8 100644 --- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift +++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift @@ -105,7 +105,7 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { return eventLoop } - @MainActor private static var didInstallGlobalExecutor = false + private nonisolated(unsafe) static var didInstallGlobalExecutor = false /// Set JavaScript event loop based executor to be the global executor /// Note that this should be called before any of the jobs are created. @@ -113,89 +113,26 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { /// introduced officially. See also [a draft proposal for custom /// executors](https://github.com/rjmccall/swift-evolution/blob/custom-executors/proposals/0000-custom-executors.md#the-default-global-concurrent-executor) public static func installGlobalExecutor() { - MainActor.assumeIsolated { - Self.installGlobalExecutorIsolated() - } + Self.installGlobalExecutorIsolated() } - @MainActor private static func installGlobalExecutorIsolated() { + private static func installGlobalExecutorIsolated() { guard !didInstallGlobalExecutor else { return } - - #if compiler(>=5.9) - typealias swift_task_asyncMainDrainQueue_hook_Fn = @convention(thin) ( - swift_task_asyncMainDrainQueue_original, swift_task_asyncMainDrainQueue_override - ) -> Void - let swift_task_asyncMainDrainQueue_hook_impl: swift_task_asyncMainDrainQueue_hook_Fn = { _, _ in - swjs_unsafe_event_loop_yield() - } - swift_task_asyncMainDrainQueue_hook = unsafeBitCast( - swift_task_asyncMainDrainQueue_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #endif - - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void - let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) - } - swift_task_enqueueGlobal_hook = unsafeBitCast( - swift_task_enqueueGlobal_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - typealias swift_task_enqueueGlobalWithDelay_hook_Fn = @convention(thin) ( - UInt64, UnownedJob, swift_task_enqueueGlobalWithDelay_original - ) -> Void - let swift_task_enqueueGlobalWithDelay_hook_impl: swift_task_enqueueGlobalWithDelay_hook_Fn = { - delay, - job, - original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: delay) - } - swift_task_enqueueGlobalWithDelay_hook = unsafeBitCast( - swift_task_enqueueGlobalWithDelay_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - #if compiler(>=5.7) - typealias swift_task_enqueueGlobalWithDeadline_hook_Fn = @convention(thin) ( - Int64, Int64, Int64, Int64, Int32, UnownedJob, swift_task_enqueueGlobalWithDelay_original - ) -> Void - let swift_task_enqueueGlobalWithDeadline_hook_impl: swift_task_enqueueGlobalWithDeadline_hook_Fn = { - sec, - nsec, - tsec, - tnsec, - clock, - job, - original in - JavaScriptEventLoop.shared.enqueue(job, withDelay: sec, nsec, tsec, tnsec, clock) + didInstallGlobalExecutor = true + #if compiler(>=6.2) + if #available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, visionOS 9999, *) { + // For Swift 6.2 and above, we can use the new `ExecutorFactory` API + _Concurrency._createExecutors(factory: JavaScriptEventLoop.self) } - swift_task_enqueueGlobalWithDeadline_hook = unsafeBitCast( - swift_task_enqueueGlobalWithDeadline_hook_impl, - to: UnsafeMutableRawPointer?.self - ) + #else + // For Swift 6.1 and below, we need to install the global executor by hook API + installByLegacyHook() #endif - - typealias swift_task_enqueueMainExecutor_hook_Fn = @convention(thin) ( - UnownedJob, swift_task_enqueueMainExecutor_original - ) -> Void - let swift_task_enqueueMainExecutor_hook_impl: swift_task_enqueueMainExecutor_hook_Fn = { job, original in - JavaScriptEventLoop.shared.unsafeEnqueue(job) - } - swift_task_enqueueMainExecutor_hook = unsafeBitCast( - swift_task_enqueueMainExecutor_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - - didInstallGlobalExecutor = true } - private func enqueue(_ job: UnownedJob, withDelay nanoseconds: UInt64) { - let milliseconds = nanoseconds / 1_000_000 + internal func enqueue(_ job: UnownedJob, withDelay milliseconds: Double) { setTimeout( - Double(milliseconds), + milliseconds, { #if compiler(>=5.9) job.runSynchronously(on: self.asUnownedSerialExecutor()) @@ -206,7 +143,19 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { ) } - private func unsafeEnqueue(_ job: UnownedJob) { + internal func unsafeEnqueue(_ job: UnownedJob) { + #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) + guard swjs_get_worker_thread_id_cached() == SWJS_MAIN_THREAD_ID else { + // Notify the main thread to execute the job when a job is + // enqueued from a Web Worker thread but without an executor preference. + // This is usually the case when hopping back to the main thread + // at the end of a task. + let jobBitPattern = unsafeBitCast(job, to: UInt.self) + swjs_send_job_to_main_thread(jobBitPattern) + return + } + // If the current thread is the main thread, do nothing special. + #endif insertJobQueue(job: job) } @@ -228,34 +177,6 @@ public final class JavaScriptEventLoop: SerialExecutor, @unchecked Sendable { } } -#if compiler(>=5.7) -/// Taken from https://github.com/apple/swift/blob/d375c972f12128ec6055ed5f5337bfcae3ec67d8/stdlib/public/Concurrency/Clock.swift#L84-L88 -@_silgen_name("swift_get_time") -internal func swift_get_time( - _ seconds: UnsafeMutablePointer, - _ nanoseconds: UnsafeMutablePointer, - _ clock: CInt -) - -@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) -extension JavaScriptEventLoop { - fileprivate func enqueue( - _ job: UnownedJob, - withDelay seconds: Int64, - _ nanoseconds: Int64, - _ toleranceSec: Int64, - _ toleranceNSec: Int64, - _ clock: Int32 - ) { - var nowSec: Int64 = 0 - var nowNSec: Int64 = 0 - swift_get_time(&nowSec, &nowNSec, clock) - let delayNanosec = (seconds - nowSec) * 1_000_000_000 + (nanoseconds - nowNSec) - enqueue(job, withDelay: delayNanosec <= 0 ? 0 : UInt64(delayNanosec)) - } -} -#endif - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) extension JSPromise { /// Wait for the promise to complete, returning (or throwing) its result. diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift index d42c5adda..82cc593bd 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift @@ -34,7 +34,7 @@ import WASILibc /// /// - SeeAlso: ``WebWorkerTaskExecutor`` @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) -public final class WebWorkerDedicatedExecutor: SerialExecutor { +public final class WebWorkerDedicatedExecutor: SerialExecutor, TaskExecutor { private let underlying: WebWorkerTaskExecutor diff --git a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift index b51445cbd..1078244f9 100644 --- a/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift +++ b/Sources/JavaScriptEventLoop/WebWorkerTaskExecutor.swift @@ -412,8 +412,9 @@ public final class WebWorkerTaskExecutor: TaskExecutor { let unmanagedContext = Unmanaged.passRetained(context) contexts.append(unmanagedContext) let ptr = unmanagedContext.toOpaque() + var thread = pthread_t(bitPattern: 0) let ret = pthread_create( - nil, + &thread, nil, { ptr in // Cast to a optional pointer to absorb nullability variations between platforms. @@ -602,78 +603,8 @@ public final class WebWorkerTaskExecutor: TaskExecutor { internal func dumpStats() {} #endif - // MARK: Global Executor hack - - @MainActor private static var _mainThread: pthread_t? - @MainActor private static var _swift_task_enqueueGlobal_hook_original: UnsafeMutableRawPointer? - @MainActor private static var _swift_task_enqueueGlobalWithDelay_hook_original: UnsafeMutableRawPointer? - @MainActor private static var _swift_task_enqueueGlobalWithDeadline_hook_original: UnsafeMutableRawPointer? - - /// Installs a global executor that forwards jobs from Web Worker threads to the main thread. - /// - /// This method sets up the necessary hooks to ensure proper task scheduling between - /// the main thread and worker threads. It must be called once (typically at application - /// startup) before using any `WebWorkerTaskExecutor` instances. - /// - /// ## Example - /// - /// ```swift - /// // At application startup - /// WebWorkerTaskExecutor.installGlobalExecutor() - /// - /// // Later, create and use executor instances - /// let executor = try await WebWorkerTaskExecutor(numberOfThreads: 4) - /// ``` - /// - /// - Important: This method must be called from the main thread. - public static func installGlobalExecutor() { - MainActor.assumeIsolated { - installGlobalExecutorIsolated() - } - } - - @MainActor - static func installGlobalExecutorIsolated() { - #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) - // Ensure this function is called only once. - guard _mainThread == nil else { return } - - _mainThread = pthread_self() - assert(swjs_get_worker_thread_id() == -1, "\(#function) must be called on the main thread") - - _swift_task_enqueueGlobal_hook_original = swift_task_enqueueGlobal_hook - - typealias swift_task_enqueueGlobal_hook_Fn = @convention(thin) (UnownedJob, swift_task_enqueueGlobal_original) - -> Void - let swift_task_enqueueGlobal_hook_impl: swift_task_enqueueGlobal_hook_Fn = { job, base in - WebWorkerTaskExecutor.traceStatsIncrement(\.enqueueGlobal) - // Enter this block only if the current Task has no executor preference. - if pthread_equal(pthread_self(), WebWorkerTaskExecutor._mainThread) != 0 { - // If the current thread is the main thread, delegate the job - // execution to the original hook of JavaScriptEventLoop. - let original = unsafeBitCast( - WebWorkerTaskExecutor._swift_task_enqueueGlobal_hook_original, - to: swift_task_enqueueGlobal_hook_Fn.self - ) - original(job, base) - } else { - // Notify the main thread to execute the job when a job is - // enqueued from a Web Worker thread but without an executor preference. - // This is usually the case when hopping back to the main thread - // at the end of a task. - WebWorkerTaskExecutor.traceStatsIncrement(\.sendJobToMainThread) - let jobBitPattern = unsafeBitCast(job, to: UInt.self) - swjs_send_job_to_main_thread(jobBitPattern) - } - } - swift_task_enqueueGlobal_hook = unsafeBitCast( - swift_task_enqueueGlobal_hook_impl, - to: UnsafeMutableRawPointer?.self - ) - #else - fatalError("Unsupported platform") - #endif - } + @available(*, deprecated, message: "Not needed anymore, just use `JavaScriptEventLoop.installGlobalExecutor()`.") + public static func installGlobalExecutor() {} } /// Enqueue a job scheduled from a Web Worker thread to the main thread. diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift index 0582fe8c4..4c441f3c4 100644 --- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift +++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift @@ -27,11 +27,6 @@ import JavaScriptEventLoop func swift_javascriptkit_activate_js_executor_impl() { MainActor.assumeIsolated { JavaScriptEventLoop.installGlobalExecutor() - #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded) - if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) { - WebWorkerTaskExecutor.installGlobalExecutor() - } - #endif } } diff --git a/Sources/JavaScriptFoundationCompat/Data+JSValue.swift b/Sources/JavaScriptFoundationCompat/Data+JSValue.swift new file mode 100644 index 000000000..ac8e773b4 --- /dev/null +++ b/Sources/JavaScriptFoundationCompat/Data+JSValue.swift @@ -0,0 +1,42 @@ +import Foundation +import JavaScriptKit + +/// Data <-> Uint8Array conversion. The conversion is lossless and copies the bytes at most once per conversion +extension Data: ConvertibleToJSValue, ConstructibleFromJSValue { + /// Convert a Data to a JSTypedArray. + /// + /// - Returns: A Uint8Array that contains the bytes of the Data. + public var jsTypedArray: JSTypedArray { + self.withUnsafeBytes { buffer in + return JSTypedArray(buffer: buffer.bindMemory(to: UInt8.self)) + } + } + + /// Convert a Data to a JSValue. + /// + /// - Returns: A JSValue that contains the bytes of the Data as a Uint8Array. + public var jsValue: JSValue { jsTypedArray.jsValue } + + /// Construct a Data from a JSTypedArray. + public static func construct(from uint8Array: JSTypedArray) -> Data? { + // First, allocate the data storage + var data = Data(count: uint8Array.lengthInBytes) + // Then, copy the byte contents into the Data buffer + data.withUnsafeMutableBytes { destinationBuffer in + uint8Array.copyMemory(to: destinationBuffer.bindMemory(to: UInt8.self)) + } + return data + } + + /// Construct a Data from a JSValue. + /// + /// - Parameter jsValue: The JSValue to construct a Data from. + /// - Returns: A Data, if the JSValue is a Uint8Array. + public static func construct(from jsValue: JSValue) -> Data? { + guard let uint8Array = JSTypedArray(from: jsValue) else { + // If the JSValue is not a Uint8Array, fail. + return nil + } + return construct(from: uint8Array) + } +} diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 7502bb5f1..24a9ae482 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -84,23 +84,24 @@ public final class JSPromise: JSBridgedClass { } #endif - #if !hasFeature(Embedded) /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult - public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func then(success: @escaping (JSValue) -> JSValue) -> JSPromise { let closure = JSOneshotClosure { success($0[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// Schedules the `success` closure to be invoked on successful completion of `self`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult - public func then(success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise { - let closure = JSOneshotClosure.async { - try await success($0[0]).jsValue + public func then( + success: sending @escaping (sending JSValue) async throws(JSException) -> JSValue + ) -> JSPromise { + let closure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + return try await success(arguments[0]) } return JSPromise(unsafelyWrapping: jsObject.then!(closure).object!) } @@ -109,8 +110,8 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then( - success: @escaping (sending JSValue) -> ConvertibleToJSValue, - failure: @escaping (sending JSValue) -> ConvertibleToJSValue + success: @escaping (sending JSValue) -> JSValue, + failure: @escaping (sending JSValue) -> JSValue ) -> JSPromise { let successClosure = JSOneshotClosure { success($0[0]).jsValue @@ -121,19 +122,19 @@ public final class JSPromise: JSBridgedClass { return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// Schedules the `success` closure to be invoked on successful completion of `self`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult public func then( - success: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue, - failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue + success: sending @escaping (sending JSValue) async throws(JSException) -> JSValue, + failure: sending @escaping (sending JSValue) async throws(JSException) -> JSValue ) -> JSPromise { - let successClosure = JSOneshotClosure.async { - try await success($0[0]).jsValue + let successClosure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + try await success(arguments[0]).jsValue } - let failureClosure = JSOneshotClosure.async { - try await failure($0[0]).jsValue + let failureClosure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + try await failure(arguments[0]).jsValue } return JSPromise(unsafelyWrapping: jsObject.then!(successClosure, failureClosure).object!) } @@ -141,21 +142,26 @@ public final class JSPromise: JSBridgedClass { /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @discardableResult - public func `catch`(failure: @escaping (sending JSValue) -> ConvertibleToJSValue) -> JSPromise { + public func `catch`( + failure: @escaping (sending JSValue) -> JSValue + ) + -> JSPromise + { let closure = JSOneshotClosure { failure($0[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } - #if compiler(>=5.5) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) /// Schedules the `failure` closure to be invoked on rejected completion of `self`. @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) @discardableResult - public func `catch`(failure: sending @escaping (sending JSValue) async throws -> ConvertibleToJSValue) -> JSPromise - { - let closure = JSOneshotClosure.async { - try await failure($0[0]).jsValue + public func `catch`( + failure: sending @escaping (sending JSValue) async throws(JSException) -> JSValue + ) -> JSPromise { + let closure = JSOneshotClosure.async { arguments throws(JSException) -> JSValue in + try await failure(arguments[0]).jsValue } return .init(unsafelyWrapping: jsObject.catch!(closure).object!) } @@ -171,5 +177,4 @@ public final class JSPromise: JSBridgedClass { } return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } - #endif } diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md index 755f68b91..e3f52885c 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Ahead-of-Time-Code-Generation.md @@ -44,7 +44,15 @@ let package = Package( ) ``` -### Step 2: Create Your Swift Code with @JS Annotations +### Step 2: Create BridgeJS Configuration + +Create a `bridge-js.config.json` file in your SwiftPM target directory you want to use BridgeJS. + +```console +$ echo "{}" > Sources/MyApp/bridge-js.config.json +``` + +### Step 3: Create Your Swift Code with @JS Annotations Write your Swift code with `@JS` annotations as usual: @@ -70,12 +78,12 @@ import JavaScriptKit } ``` -### Step 3: Create Your TypeScript Definitions +### Step 4: Create Your TypeScript Definitions -If you're importing JavaScript APIs, create your `bridge.d.ts` file as usual: +If you're importing JavaScript APIs, create your `bridge-js.d.ts` file as usual: ```typescript -// Sources/MyApp/bridge.d.ts +// Sources/MyApp/bridge-js.d.ts export function consoleLog(message: string): void; export interface Document { @@ -86,7 +94,7 @@ export interface Document { export function getDocument(): Document; ``` -### Step 4: Generate the Bridge Code +### Step 5: Generate the Bridge Code Run the command plugin to generate the bridge code: @@ -108,7 +116,7 @@ Sources/MyApp/Generated/ImportTS.swift # Generated code for TypeScript impor Sources/MyApp/Generated/JavaScript/ # Generated JSON skeletons ``` -### Step 5: Add Generated Files to Version Control +### Step 6: Add Generated Files to Version Control Add these generated files to your version control system: @@ -117,7 +125,7 @@ git add Sources/MyApp/Generated git commit -m "Add generated BridgeJS code" ``` -### Step 6: Build Your Package +### Step 7: Build Your Package Now you can build your package as usual: diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md index 5f9bb4a12..98a9c80cb 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/Importing-TypeScript-into-Swift.md @@ -51,7 +51,7 @@ let package = Package( ### Step 2: Create TypeScript Definitions -Create a file named `bridge.d.ts` in your target source directory (e.g. `Sources//bridge.d.ts`). This file defines the JavaScript APIs you want to use in Swift: +Create a file named `bridge-js.d.ts` in your target source directory (e.g. `Sources//bridge-js.d.ts`). This file defines the JavaScript APIs you want to use in Swift: ```typescript // Simple function diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index fa713c3b9..18a400786 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -1,4 +1,7 @@ import _CJavaScriptKit +#if hasFeature(Embedded) && os(WASI) +import _Concurrency +#endif /// `JSClosureProtocol` wraps Swift closure objects for use in JavaScript. Conforming types /// are responsible for managing the lifetime of the closure they wrap, but can delegate that @@ -40,10 +43,11 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { fatalError("JSOneshotClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) && !hasFeature(Embedded) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure - { + public static func async( + _ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue + ) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) } #endif @@ -132,9 +136,11 @@ public class JSClosure: JSFunction, JSClosureProtocol { fatalError("JSClosure does not support dictionary literal initialization") } - #if compiler(>=5.5) && !hasFeature(Embedded) + #if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) - public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure { + public static func async( + _ body: @Sendable @escaping (sending [JSValue]) async throws(JSException) -> JSValue + ) -> JSClosure { JSClosure(makeAsyncClosure(body)) } #endif @@ -148,10 +154,10 @@ public class JSClosure: JSFunction, JSClosureProtocol { #endif } -#if compiler(>=5.5) && !hasFeature(Embedded) +#if compiler(>=5.5) && (!hasFeature(Embedded) || os(WASI)) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private func makeAsyncClosure( - _ body: sending @escaping (sending [JSValue]) async throws -> JSValue + _ body: sending @escaping (sending [JSValue]) async throws(JSException) -> JSValue ) -> ((sending [JSValue]) -> JSValue) { { arguments in JSPromise { resolver in @@ -161,19 +167,15 @@ private func makeAsyncClosure( struct Context: @unchecked Sendable { let resolver: (JSPromise.Result) -> Void let arguments: [JSValue] - let body: (sending [JSValue]) async throws -> JSValue + let body: (sending [JSValue]) async throws(JSException) -> JSValue } let context = Context(resolver: resolver, arguments: arguments, body: body) Task { - do { + do throws(JSException) { let result = try await context.body(context.arguments) context.resolver(.success(result)) } catch { - if let jsError = error as? JSException { - context.resolver(.failure(jsError.thrownValue)) - } else { - context.resolver(.failure(JSError(message: String(describing: error)).jsValue)) - } + context.resolver(.failure(error.thrownValue)) } } }.jsValue() diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 931b48f7a..d587478a5 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -326,6 +326,8 @@ IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) IMPORT_JS_FUNCTION(swjs_create_object, JavaScriptObjectRef, (void)) +#define SWJS_MAIN_THREAD_ID -1 + int swjs_get_worker_thread_id_cached(void); /// Requests sending a JavaScript object to another worker thread. diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 1473594e5..2b78b96b5 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -5,6 +5,10 @@ import JavaScriptKit @_extern(c) func runJsWorks() -> Void +@JS func roundTripVoid() -> Void { + return +} + @JS func roundTripInt(v: Int) -> Int { return v } @@ -24,6 +28,27 @@ func runJsWorks() -> Void return v } +@JS func roundTripJSObject(v: JSObject) -> JSObject { + return v +} + +struct TestError: Error { + let message: String +} + +@JS func throwsSwiftError(shouldThrow: Bool) throws(JSException) -> Void { + if shouldThrow { + throw JSException(JSError(message: "TestError").jsValue) + } +} +@JS func throwsWithIntResult() throws(JSException) -> Int { return 1 } +@JS func throwsWithStringResult() throws(JSException) -> String { return "Ok" } +@JS func throwsWithBoolResult() throws(JSException) -> Bool { return true } +@JS func throwsWithFloatResult() throws(JSException) -> Float { return 1.0 } +@JS func throwsWithDoubleResult() throws(JSException) -> Double { return 1.0 } +@JS func throwsWithSwiftHeapObjectResult() throws(JSException) -> Greeter { return Greeter(name: "Test") } +@JS func throwsWithJSObjectResult() throws(JSException) -> JSObject { return JSObject() } + @JS class Greeter { var name: String diff --git a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift index cc3c9df31..363bf2d9f 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/ExportSwift.swift @@ -1,39 +1,81 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) @_extern(wasm, module: "bjs", name: "return_string") private func _return_string(_ ptr: UnsafePointer?, _ len: Int32) @_extern(wasm, module: "bjs", name: "init_memory") private func _init_memory(_ sourceId: Int32, _ ptr: UnsafeMutablePointer?) +@_extern(wasm, module: "bjs", name: "swift_js_retain") +private func _swift_js_retain(_ ptr: Int32) -> Int32 +@_extern(wasm, module: "bjs", name: "swift_js_throw") +private func _swift_js_throw(_ id: Int32) +#endif + +@_expose(wasm, "bjs_roundTripVoid") +@_cdecl("bjs_roundTripVoid") +public func _bjs_roundTripVoid() -> Void { + #if arch(wasm32) + roundTripVoid() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_roundTripInt") @_cdecl("bjs_roundTripInt") public func _bjs_roundTripInt(v: Int32) -> Int32 { + #if arch(wasm32) let ret = roundTripInt(v: Int(v)) return Int32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripFloat") @_cdecl("bjs_roundTripFloat") public func _bjs_roundTripFloat(v: Float32) -> Float32 { + #if arch(wasm32) let ret = roundTripFloat(v: v) return Float32(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripDouble") @_cdecl("bjs_roundTripDouble") public func _bjs_roundTripDouble(v: Float64) -> Float64 { + #if arch(wasm32) let ret = roundTripDouble(v: v) return Float64(ret) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripBool") @_cdecl("bjs_roundTripBool") public func _bjs_roundTripBool(v: Int32) -> Int32 { + #if arch(wasm32) let ret = roundTripBool(v: v == 1) return Int32(ret ? 1 : 0) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripString") @_cdecl("bjs_roundTripString") public func _bjs_roundTripString(vBytes: Int32, vLen: Int32) -> Void { + #if arch(wasm32) let v = String(unsafeUninitializedCapacity: Int(vLen)) { b in _init_memory(vBytes, b.baseAddress.unsafelyUnwrapped) return Int(vLen) @@ -42,53 +84,288 @@ public func _bjs_roundTripString(vBytes: Int32, vLen: Int32) -> Void { return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_roundTripSwiftHeapObject") @_cdecl("bjs_roundTripSwiftHeapObject") public func _bjs_roundTripSwiftHeapObject(v: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + #if arch(wasm32) let ret = roundTripSwiftHeapObject(v: Unmanaged.fromOpaque(v).takeUnretainedValue()) return Unmanaged.passRetained(ret).toOpaque() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_roundTripJSObject") +@_cdecl("bjs_roundTripJSObject") +public func _bjs_roundTripJSObject(v: Int32) -> Int32 { + #if arch(wasm32) + let ret = roundTripJSObject(v: JSObject(id: UInt32(bitPattern: v))) + return _swift_js_retain(Int32(bitPattern: ret.id)) + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsSwiftError") +@_cdecl("bjs_throwsSwiftError") +public func _bjs_throwsSwiftError(shouldThrow: Int32) -> Void { + #if arch(wasm32) + do { + try throwsSwiftError(shouldThrow: shouldThrow == 1) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithIntResult") +@_cdecl("bjs_throwsWithIntResult") +public func _bjs_throwsWithIntResult() -> Int32 { + #if arch(wasm32) + do { + let ret = try throwsWithIntResult() + return Int32(ret) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithStringResult") +@_cdecl("bjs_throwsWithStringResult") +public func _bjs_throwsWithStringResult() -> Void { + #if arch(wasm32) + do { + var ret = try throwsWithStringResult() + return ret.withUTF8 { ptr in + _return_string(ptr.baseAddress, Int32(ptr.count)) + } + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithBoolResult") +@_cdecl("bjs_throwsWithBoolResult") +public func _bjs_throwsWithBoolResult() -> Int32 { + #if arch(wasm32) + do { + let ret = try throwsWithBoolResult() + return Int32(ret ? 1 : 0) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithFloatResult") +@_cdecl("bjs_throwsWithFloatResult") +public func _bjs_throwsWithFloatResult() -> Float32 { + #if arch(wasm32) + do { + let ret = try throwsWithFloatResult() + return Float32(ret) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0.0 + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithDoubleResult") +@_cdecl("bjs_throwsWithDoubleResult") +public func _bjs_throwsWithDoubleResult() -> Float64 { + #if arch(wasm32) + do { + let ret = try throwsWithDoubleResult() + return Float64(ret) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0.0 + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithSwiftHeapObjectResult") +@_cdecl("bjs_throwsWithSwiftHeapObjectResult") +public func _bjs_throwsWithSwiftHeapObjectResult() -> UnsafeMutableRawPointer { + #if arch(wasm32) + do { + let ret = try throwsWithSwiftHeapObjectResult() + return Unmanaged.passRetained(ret).toOpaque() + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return UnsafeMutableRawPointer(bitPattern: -1).unsafelyUnwrapped + } + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_throwsWithJSObjectResult") +@_cdecl("bjs_throwsWithJSObjectResult") +public func _bjs_throwsWithJSObjectResult() -> Int32 { + #if arch(wasm32) + do { + let ret = try throwsWithJSObjectResult() + return _swift_js_retain(Int32(bitPattern: ret.id)) + } catch let error { + if let error = error.thrownValue.object { + withExtendedLifetime(error) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } else { + let jsError = JSError(message: String(describing: error)) + withExtendedLifetime(jsError.jsObject) { + _swift_js_throw(Int32(bitPattern: $0.id)) + } + } + return 0 + } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_takeGreeter") @_cdecl("bjs_takeGreeter") public func _bjs_takeGreeter(g: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } takeGreeter(g: Unmanaged.fromOpaque(g).takeUnretainedValue(), name: name) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_init") @_cdecl("bjs_Greeter_init") public func _bjs_Greeter_init(nameBytes: Int32, nameLen: Int32) -> UnsafeMutableRawPointer { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } let ret = Greeter(name: name) return Unmanaged.passRetained(ret).toOpaque() + #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) var ret = Unmanaged.fromOpaque(_self).takeUnretainedValue().greet() return ret.withUTF8 { ptr in _return_string(ptr.baseAddress, Int32(ptr.count)) } + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_changeName") @_cdecl("bjs_Greeter_changeName") public func _bjs_Greeter_changeName(_self: UnsafeMutableRawPointer, nameBytes: Int32, nameLen: Int32) -> Void { + #if arch(wasm32) let name = String(unsafeUninitializedCapacity: Int(nameLen)) { b in _init_memory(nameBytes, b.baseAddress.unsafelyUnwrapped) return Int(nameLen) } Unmanaged.fromOpaque(_self).takeUnretainedValue().changeName(name: name) + #else + fatalError("Only available on WebAssembly") + #endif } @_expose(wasm, "bjs_Greeter_deinit") diff --git a/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift new file mode 100644 index 000000000..35148cf57 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/Generated/ImportTS.swift @@ -0,0 +1,200 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +@_spi(JSObject_id) import JavaScriptKit + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "make_jsstring") +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 +#else +func _make_jsstring(_ ptr: UnsafePointer?, _ len: Int32) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "init_memory_with_result") +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) +#else +func _init_memory_with_result(_ ptr: UnsafePointer?, _ len: Int32) { + fatalError("Only available on WebAssembly") +} +#endif + +func jsRoundTripVoid() -> Void { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripVoid") + func bjs_jsRoundTripVoid() -> Void + #else + func bjs_jsRoundTripVoid() -> Void { + fatalError("Only available on WebAssembly") + } + #endif + bjs_jsRoundTripVoid() +} + +func jsRoundTripNumber(_ v: Double) -> Double { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripNumber") + func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 + #else + func bjs_jsRoundTripNumber(_ v: Float64) -> Float64 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_jsRoundTripNumber(v) + return Double(ret) +} + +func jsRoundTripBool(_ v: Bool) -> Bool { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripBool") + func bjs_jsRoundTripBool(_ v: Int32) -> Int32 + #else + func bjs_jsRoundTripBool(_ v: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_jsRoundTripBool(Int32(v ? 1 : 0)) + return ret == 1 +} + +func jsRoundTripString(_ v: String) -> String { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripString") + func bjs_jsRoundTripString(_ v: Int32) -> Int32 + #else + func bjs_jsRoundTripString(_ v: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + var v = v + let vId = v.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + let ret = bjs_jsRoundTripString(vId) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } +} + +struct JsGreeter { + let this: JSObject + + init(this: JSObject) { + self.this = this + } + + init(takingThis this: Int32) { + self.this = JSObject(id: UInt32(bitPattern: this)) + } + + init(_ name: String, _ prefix: String) { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_init") + func bjs_JsGreeter_init(_ name: Int32, _ prefix: Int32) -> Int32 + #else + func bjs_JsGreeter_init(_ name: Int32, _ prefix: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + var prefix = prefix + let prefixId = prefix.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + let ret = bjs_JsGreeter_init(nameId, prefixId) + self.this = JSObject(id: UInt32(bitPattern: ret)) + } + + var name: String { + get { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_name_get") + func bjs_JsGreeter_name_get(_ self: Int32) -> Int32 + #else + func bjs_JsGreeter_name_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_JsGreeter_name_get(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + nonmutating set { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_name_set") + func bjs_JsGreeter_name_set(_ self: Int32, _ newValue: Int32) -> Void + #else + func bjs_JsGreeter_name_set(_ self: Int32, _ newValue: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + var newValue = newValue + let newValueId = newValue.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_JsGreeter_name_set(Int32(bitPattern: self.this.id), newValueId) + } + } + + var prefix: String { + get { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_prefix_get") + func bjs_JsGreeter_prefix_get(_ self: Int32) -> Int32 + #else + func bjs_JsGreeter_prefix_get(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_JsGreeter_prefix_get(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + } + + func greet() -> String { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_greet") + func bjs_JsGreeter_greet(_ self: Int32) -> Int32 + #else + func bjs_JsGreeter_greet(_ self: Int32) -> Int32 { + fatalError("Only available on WebAssembly") + } + #endif + let ret = bjs_JsGreeter_greet(Int32(bitPattern: self.this.id)) + return String(unsafeUninitializedCapacity: Int(ret)) { b in + _init_memory_with_result(b.baseAddress.unsafelyUnwrapped, Int32(ret)) + return Int(ret) + } + } + + func changeName(_ name: String) -> Void { + #if arch(wasm32) + @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_JsGreeter_changeName") + func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void + #else + func bjs_JsGreeter_changeName(_ self: Int32, _ name: Int32) -> Void { + fatalError("Only available on WebAssembly") + } + #endif + var name = name + let nameId = name.withUTF8 { b in + _make_jsstring(b.baseAddress.unsafelyUnwrapped, Int32(b.count)) + } + bjs_JsGreeter_changeName(Int32(bitPattern: self.this.id), nameId) + } + +} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json index f60426a09..7a467cc30 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ExportSwift.json @@ -3,6 +3,10 @@ { "constructor" : { "abiName" : "bjs_Greeter_init", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "parameters" : [ { "label" : "name", @@ -18,6 +22,10 @@ "methods" : [ { "abiName" : "bjs_Greeter_greet", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "greet", "parameters" : [ @@ -30,6 +38,10 @@ }, { "abiName" : "bjs_Greeter_changeName", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "changeName", "parameters" : [ { @@ -53,8 +65,28 @@ } ], "functions" : [ + { + "abiName" : "bjs_roundTripVoid", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "roundTripVoid", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, { "abiName" : "bjs_roundTripInt", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripInt", "parameters" : [ { @@ -75,6 +107,10 @@ }, { "abiName" : "bjs_roundTripFloat", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripFloat", "parameters" : [ { @@ -95,6 +131,10 @@ }, { "abiName" : "bjs_roundTripDouble", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripDouble", "parameters" : [ { @@ -115,6 +155,10 @@ }, { "abiName" : "bjs_roundTripBool", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripBool", "parameters" : [ { @@ -135,6 +179,10 @@ }, { "abiName" : "bjs_roundTripString", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripString", "parameters" : [ { @@ -155,6 +203,10 @@ }, { "abiName" : "bjs_roundTripSwiftHeapObject", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "roundTripSwiftHeapObject", "parameters" : [ { @@ -173,8 +225,172 @@ } } }, + { + "abiName" : "bjs_roundTripJSObject", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, + "name" : "roundTripJSObject", + "parameters" : [ + { + "label" : "v", + "name" : "v", + "type" : { + "jsObject" : { + + } + } + } + ], + "returnType" : { + "jsObject" : { + + } + } + }, + { + "abiName" : "bjs_throwsSwiftError", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsSwiftError", + "parameters" : [ + { + "label" : "shouldThrow", + "name" : "shouldThrow", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithIntResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithIntResult", + "parameters" : [ + + ], + "returnType" : { + "int" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithStringResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithStringResult", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithBoolResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithBoolResult", + "parameters" : [ + + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithFloatResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithFloatResult", + "parameters" : [ + + ], + "returnType" : { + "float" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithDoubleResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithDoubleResult", + "parameters" : [ + + ], + "returnType" : { + "double" : { + + } + } + }, + { + "abiName" : "bjs_throwsWithSwiftHeapObjectResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithSwiftHeapObjectResult", + "parameters" : [ + + ], + "returnType" : { + "swiftHeapObject" : { + "_0" : "Greeter" + } + } + }, + { + "abiName" : "bjs_throwsWithJSObjectResult", + "effects" : { + "isAsync" : false, + "isThrows" : true + }, + "name" : "throwsWithJSObjectResult", + "parameters" : [ + + ], + "returnType" : { + "jsObject" : { + + } + } + }, { "abiName" : "bjs_takeGreeter", + "effects" : { + "isAsync" : false, + "isThrows" : false + }, "name" : "takeGreeter", "parameters" : [ { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json new file mode 100644 index 000000000..ad8fcd875 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/ImportTS.json @@ -0,0 +1,150 @@ +{ + "children" : [ + { + "functions" : [ + { + "name" : "jsRoundTripVoid", + "parameters" : [ + + ], + "returnType" : { + "void" : { + + } + } + }, + { + "name" : "jsRoundTripNumber", + "parameters" : [ + { + "name" : "v", + "type" : { + "double" : { + + } + } + } + ], + "returnType" : { + "double" : { + + } + } + }, + { + "name" : "jsRoundTripBool", + "parameters" : [ + { + "name" : "v", + "type" : { + "bool" : { + + } + } + } + ], + "returnType" : { + "bool" : { + + } + } + }, + { + "name" : "jsRoundTripString", + "parameters" : [ + { + "name" : "v", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "string" : { + + } + } + } + ], + "types" : [ + { + "constructor" : { + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "name" : "prefix", + "type" : { + "string" : { + + } + } + } + ] + }, + "methods" : [ + { + "name" : "greet", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, + { + "name" : "changeName", + "parameters" : [ + { + "name" : "name", + "type" : { + "string" : { + + } + } + } + ], + "returnType" : { + "void" : { + + } + } + } + ], + "name" : "JsGreeter", + "properties" : [ + { + "isReadonly" : false, + "name" : "name", + "type" : { + "string" : { + + } + } + }, + { + "isReadonly" : true, + "name" : "prefix", + "type" : { + "string" : { + + } + } + } + ] + } + ] + } + ], + "moduleName" : "BridgeJSRuntimeTests" +} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/ImportAPITests.swift b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift new file mode 100644 index 000000000..a8d586bff --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/ImportAPITests.swift @@ -0,0 +1,50 @@ +import XCTest +import JavaScriptKit + +class ImportAPITests: XCTestCase { + func testRoundTripVoid() { + jsRoundTripVoid() + } + + func testRoundTripNumber() { + for v in [ + 0, 1, -1, + Double(Int32.max), Double(Int32.min), + Double(Int64.max), Double(Int64.min), + Double(UInt32.max), Double(UInt32.min), + Double(UInt64.max), Double(UInt64.min), + Double.greatestFiniteMagnitude, Double.leastNonzeroMagnitude, + Double.infinity, + Double.pi, + ] { + XCTAssertEqual(jsRoundTripNumber(v), v) + } + + XCTAssert(jsRoundTripNumber(Double.nan).isNaN) + } + + func testRoundTripBool() { + for v in [true, false] { + XCTAssertEqual(jsRoundTripBool(v), v) + } + } + + func testRoundTripString() { + for v in ["", "Hello, world!", "🧑‍🧑‍🧒"] { + XCTAssertEqual(jsRoundTripString(v), v) + } + } + + func testClass() { + let greeter = JsGreeter("Alice", "Hello") + XCTAssertEqual(greeter.greet(), "Hello, Alice!") + greeter.changeName("Bob") + XCTAssertEqual(greeter.greet(), "Hello, Bob!") + + greeter.name = "Charlie" + XCTAssertEqual(greeter.greet(), "Hello, Charlie!") + XCTAssertEqual(greeter.name, "Charlie") + + XCTAssertEqual(greeter.prefix, "Hello") + } +} diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.config.json b/Tests/BridgeJSRuntimeTests/bridge-js.config.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/bridge-js.config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Tests/BridgeJSRuntimeTests/bridge-js.d.ts b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts new file mode 100644 index 000000000..664dd4471 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/bridge-js.d.ts @@ -0,0 +1,12 @@ +export function jsRoundTripVoid(): void +export function jsRoundTripNumber(v: number): number +export function jsRoundTripBool(v: boolean): boolean +export function jsRoundTripString(v: string): string + +export class JsGreeter { + name: string; + readonly prefix: string; + constructor(name: string, prefix: string); + greet(): string; + changeName(name: string): void; +} \ No newline at end of file diff --git a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift index 962b04421..c3429e8c9 100644 --- a/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift +++ b/Tests/JavaScriptEventLoopTests/JSPromiseTests.swift @@ -9,14 +9,14 @@ final class JSPromiseTests: XCTestCase { p1 = p1.then { value in XCTAssertEqual(value, .null) continuation.resume() - return JSValue.number(1.0) + return JSValue.number(1.0).jsValue } } await withCheckedContinuation { continuation in p1 = p1.then { value in XCTAssertEqual(value, .number(1.0)) continuation.resume() - return JSPromise.resolve(JSValue.boolean(true)) + return JSPromise.resolve(JSValue.boolean(true)).jsValue } } await withCheckedContinuation { continuation in @@ -48,7 +48,7 @@ final class JSPromiseTests: XCTestCase { p2 = p2.then { value in XCTAssertEqual(value, .boolean(true)) continuation.resume() - return JSPromise.reject(JSValue.number(2.0)) + return JSPromise.reject(JSValue.number(2.0)).jsValue } } await withCheckedContinuation { continuation in diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 1da56e680..8fbbd817f 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -150,14 +150,14 @@ final class JavaScriptEventLoopTests: XCTestCase { ) } let promise2 = promise.then { result in - try await Task.sleep(nanoseconds: 100_000_000) - return String(result.number!) + try! await Task.sleep(nanoseconds: 100_000_000) + return .string(String(result.number!)) } let thenDiff = try await measureTime { let result = try await promise2.value XCTAssertEqual(result, .string("3.0")) } - XCTAssertGreaterThanOrEqual(thenDiff, 200) + XCTAssertGreaterThanOrEqual(thenDiff, 150) } func testPromiseThenWithFailure() async throws { @@ -171,8 +171,8 @@ final class JavaScriptEventLoopTests: XCTestCase { 100 ) } - let failingPromise2 = failingPromise.then { _ in - throw MessageError("Should not be called", file: #file, line: #line, column: #column) + let failingPromise2 = failingPromise.then { _ -> JSValue in + fatalError("Should not be called") } failure: { err in return err } @@ -192,7 +192,7 @@ final class JavaScriptEventLoopTests: XCTestCase { ) } let catchPromise2 = catchPromise.catch { err in - try await Task.sleep(nanoseconds: 100_000_000) + try! await Task.sleep(nanoseconds: 100_000_000) return err } let catchDiff = try await measureTime { @@ -225,7 +225,7 @@ final class JavaScriptEventLoopTests: XCTestCase { func testAsyncJSClosure() async throws { // Test Async JSClosure let delayClosure = JSClosure.async { _ -> JSValue in - try await Task.sleep(nanoseconds: 200_000_000) + try! await Task.sleep(nanoseconds: 200_000_000) return JSValue.number(3) } let delayObject = JSObject.global.Object.function!.new() diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift index acc6fccf9..f743d8ef0 100644 --- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift +++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift @@ -90,9 +90,7 @@ final class WebWorkerTaskExecutorTests: XCTestCase { } } let taskRunOnMainThread = await task.value - // FIXME: The block passed to `MainActor.run` should run on the main thread - // XCTAssertTrue(taskRunOnMainThread) - XCTAssertFalse(taskRunOnMainThread) + XCTAssertTrue(taskRunOnMainThread) // After the task is done, back to the main thread XCTAssertTrue(isMainThread()) diff --git a/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift b/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift new file mode 100644 index 000000000..8c0d6162d --- /dev/null +++ b/Tests/JavaScriptFoundationCompatTests/Data+JSValueTests.swift @@ -0,0 +1,55 @@ +import XCTest +import Foundation +import JavaScriptFoundationCompat +import JavaScriptKit + +final class DataJSValueTests: XCTestCase { + func testDataToJSValue() { + let data = Data([0x00, 0x01, 0x02, 0x03]) + let jsValue = data.jsValue + + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 4) + XCTAssertEqual(uint8Array?[0], 0x00) + XCTAssertEqual(uint8Array?[1], 0x01) + XCTAssertEqual(uint8Array?[2], 0x02) + XCTAssertEqual(uint8Array?[3], 0x03) + } + + func testJSValueToData() { + let jsValue = JSTypedArray([0x00, 0x01, 0x02, 0x03]).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data, Data([0x00, 0x01, 0x02, 0x03])) + } + + func testDataToJSValue_withLargeData() { + let data = Data(repeating: 0x00, count: 1024 * 1024) + let jsValue = data.jsValue + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 1024 * 1024) + } + + func testJSValueToData_withLargeData() { + let jsValue = JSTypedArray(Array(repeating: 0x00, count: 1024 * 1024)).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data?.count, 1024 * 1024) + } + + func testDataToJSValue_withEmptyData() { + let data = Data() + let jsValue = data.jsValue + let uint8Array = JSTypedArray(from: jsValue) + XCTAssertEqual(uint8Array?.lengthInBytes, 0) + } + + func testJSValueToData_withEmptyData() { + let jsValue = JSTypedArray([]).jsValue + let data = Data.construct(from: jsValue) + XCTAssertEqual(data, Data()) + } + + func testJSValueToData_withInvalidJSValue() { + let data = Data.construct(from: JSObject().jsValue) + XCTAssertNil(data) + } +} diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 1e12d3755..4a28d6aa5 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,27 +1,62 @@ -/** @type {import('./../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').Prelude["setupOptions"]} */ -export function setupOptions(options, context) { +// @ts-check + +/** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ +export async function setupOptions(options, context) { Error.stackTraceLimit = 100; setupTestGlobals(globalThis); return { ...options, - addToCoreImports(importObject, getInstance, getExports) { - options.addToCoreImports?.(importObject); + imports: { + "jsRoundTripVoid": () => { + return; + }, + "jsRoundTripNumber": (v) => { + return v; + }, + "jsRoundTripBool": (v) => { + return v; + }, + "jsRoundTripString": (v) => { + return v; + }, + JsGreeter: class { + /** + * @param {string} name + * @param {string} prefix + */ + constructor(name, prefix) { + this.name = name; + this.prefix = prefix; + } + greet() { + return `${this.prefix}, ${this.name}!`; + } + /** @param {string} name */ + changeName(name) { + this.name = name; + } + } + }, + addToCoreImports(importObject, importsContext) { + const { getInstance, getExports } = importsContext; + options.addToCoreImports?.(importObject, importsContext); importObject["JavaScriptEventLoopTestSupportTests"] = { "isMainThread": () => context.isMainThread, } - importObject["BridgeJSRuntimeTests"] = { - "runJsWorks": () => { - return BridgeJSRuntimeTests_runJsWorks(getInstance(), getExports()); - }, + const bridgeJSRuntimeTests = importObject["BridgeJSRuntimeTests"] || {}; + bridgeJSRuntimeTests["runJsWorks"] = () => { + return BridgeJSRuntimeTests_runJsWorks(getInstance(), getExports()); } + importObject["BridgeJSRuntimeTests"] = bridgeJSRuntimeTests; } } } import assert from "node:assert"; -/** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge.d.ts').Exports} exports */ +/** @param {import('./../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.d.ts').Exports} exports */ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { + exports.roundTripVoid(); for (const v of [0, 1, -1, 2147483647, -2147483648]) { assert.equal(exports.roundTripInt(v), v); } @@ -67,6 +102,22 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { exports.takeGreeter(g, "Jay"); assert.equal(g.greet(), "Hello, Jay!"); g.release(); + + const anyObject = {}; + assert.equal(exports.roundTripJSObject(anyObject), anyObject); + + try { + exports.throwsSwiftError(true); + assert.fail("Expected error"); + } catch (error) { + assert.equal(error.message, "TestError", error); + } + + try { + exports.throwsSwiftError(false); + } catch (error) { + assert.fail("Expected no error"); + } } function setupTestGlobals(global) {