diff --git a/Examples/Sources/Factorial/Factorial.swift b/Examples/Sources/Factorial/Factorial.swift index 5233362c..9f604a9d 100644 --- a/Examples/Sources/Factorial/Factorial.swift +++ b/Examples/Sources/Factorial/Factorial.swift @@ -13,11 +13,13 @@ struct Example { ) // Create a module instance from the parsed module. - let runtime = Runtime() - let instance = try runtime.instantiate(module: module) + let engine = Engine() + let store = Store(engine: engine) + let instance = try module.instantiate(store: store) let input: UInt64 = 5 // Invoke the exported function "fac" with a single argument. - let result = try runtime.invoke(instance, function: "fac", with: [.i64(input)]) + let fac = instance.exports[function: "fac"]! + let result = try fac([.i64(input)]) print("fac(\(input)) = \(result[0].i64)") } } diff --git a/Examples/Sources/PrintAdd/PrintAdd.swift b/Examples/Sources/PrintAdd/PrintAdd.swift index 859e139c..2a502779 100644 --- a/Examples/Sources/PrintAdd/PrintAdd.swift +++ b/Examples/Sources/PrintAdd/PrintAdd.swift @@ -20,18 +20,26 @@ struct Example { ) ) - // Define a host function that prints an i32 value. - let hostPrint = HostFunction(type: FunctionType(parameters: [.i32])) { _, args in - // This function is called from "print_add" in the WebAssembly module. - print(args[0]) - return [] - } - // Create a runtime importing the host function. - let runtime = Runtime(hostModules: [ - "printer": HostModule(functions: ["print_i32": hostPrint]) - ]) - let instance = try runtime.instantiate(module: module) + // Create engine and store + let engine = Engine() + let store = Store(engine: engine) + + // Instantiate a parsed module with importing a host function + let instance = try module.instantiate( + store: store, + // Import a host function that prints an i32 value. + imports: [ + "printer": [ + "print_i32": Function(store: store, parameters: [.i32]) { _, args in + // This function is called from "print_add" in the WebAssembly module. + print(args[0]) + return [] + } + ] + ] + ) // Invoke the exported function "print_add" - _ = try runtime.invoke(instance, function: "print_add", with: [.i32(42), .i32(3)]) + let printAdd = instance.exports[function: "print_add"]! + try printAdd([.i32(42), .i32(3)]) } } diff --git a/Examples/Sources/WASI-Hello/Hello.swift b/Examples/Sources/WASI-Hello/Hello.swift index 7b0f75f0..43056f5f 100644 --- a/Examples/Sources/WASI-Hello/Hello.swift +++ b/Examples/Sources/WASI-Hello/Hello.swift @@ -12,12 +12,16 @@ struct Example { // Create a WASI instance forwarding to the host environment. let wasi = try WASIBridgeToHost() - // Create a runtime with WASI host modules. - let runtime = Runtime(hostModules: wasi.hostModules) - let instance = try runtime.instantiate(module: module) + // Create engine and store + let engine = Engine() + let store = Store(engine: engine) + // Instantiate a parsed module importing WASI + var imports = Imports() + wasi.link(to: &imports, store: store) + let instance = try module.instantiate(store: store, imports: imports) // Start the WASI command-line application. - let exitCode = try wasi.start(instance, runtime: runtime) + let exitCode = try wasi.start(instance) // Exit the Swift program with the WASI exit code. exit(Int32(exitCode)) } diff --git a/FuzzTesting/Sources/FuzzDifferential/FuzzDifferential.swift b/FuzzTesting/Sources/FuzzDifferential/FuzzDifferential.swift index 493699ba..ce07d384 100644 --- a/FuzzTesting/Sources/FuzzDifferential/FuzzDifferential.swift +++ b/FuzzTesting/Sources/FuzzDifferential/FuzzDifferential.swift @@ -92,9 +92,10 @@ extension wasm_name_t { struct WasmKitEngine: Engine { func run(moduleBytes: [UInt8]) throws -> ExecResult { let module = try WasmKit.parseWasm(bytes: moduleBytes) - let runtime = Runtime() - let instance = try runtime.instantiate(module: module) - let exports = instance.exports.sorted(by: { $0.key < $1.key }) + let engine = WasmKit.Engine() + let store = WasmKit.Store(engine: engine) + let instance = try module.instantiate(store: store) + let exports = instance.exports.sorted(by: { $0.name < $1.name }) let memories: [Memory] = exports.compactMap { guard case let .memory(memory) = $0.value else { return nil @@ -117,7 +118,7 @@ struct WasmKitEngine: Engine { let type = fn.type let arguments = type.parameters.map { $0.defaultValue } do { - let results = try fn.invoke(arguments, runtime: runtime) + let results = try fn(arguments) return ExecResult(values: results, trap: nil, memory: memory?.data) } catch { return ExecResult(values: nil, trap: String(describing: error), memory: memory?.data) diff --git a/FuzzTesting/Sources/FuzzExecute/FuzzExecute.swift b/FuzzTesting/Sources/FuzzExecute/FuzzExecute.swift index 49c6e31b..f739a61c 100644 --- a/FuzzTesting/Sources/FuzzExecute/FuzzExecute.swift +++ b/FuzzTesting/Sources/FuzzExecute/FuzzExecute.swift @@ -14,16 +14,17 @@ public func FuzzCheck(_ start: UnsafePointer, _ count: Int) -> CInt { let bytes = Array(UnsafeBufferPointer(start: start, count: count)) do { let module = try WasmKit.parseWasm(bytes: bytes) - let runtime = WasmKit.Runtime() - runtime.store.resourceLimiter = FuzzerResourceLimiter() - let instance = try runtime.instantiate(module: module) + let engine = WasmKit.Engine() + let store = WasmKit.Store(engine: engine) + store.resourceLimiter = FuzzerResourceLimiter() + let instance = try module.instantiate(store: store) for export in instance.exports.values { guard case let .function(fn) = export else { continue } let type = fn.type let arguments = type.parameters.map { $0.defaultValue } - _ = try fn.invoke(arguments, runtime: runtime) + _ = try fn(arguments) } } catch { // Ignore errors diff --git a/Sources/CLI/Commands/Explore.swift b/Sources/CLI/Commands/Explore.swift index bd8de29a..f6d01c5c 100644 --- a/Sources/CLI/Commands/Explore.swift +++ b/Sources/CLI/Commands/Explore.swift @@ -14,25 +14,29 @@ struct Explore: ParsableCommand { func run() throws { let module = try parseWasm(filePath: FilePath(path)) - var hostModuleStubs: [String: HostModule] = [:] + // Instruction dumping requires token threading model for now + let configuration = EngineConfiguration(threadingModel: .token) + let engine = Engine(configuration: configuration) + let store = Store(engine: engine) + + var imports: Imports = [:] for importEntry in module.imports { - var hostModule = hostModuleStubs[importEntry.module] ?? HostModule() switch importEntry.descriptor { case .function(let typeIndex): let type = module.types[Int(typeIndex)] - hostModule.functions[importEntry.name] = HostFunction(type: type) { _, _ in - fatalError("unreachable") - } + imports.define( + module: importEntry.module, + name: importEntry.name, + Function(store: store, type: type) { _, _ in + fatalError("unreachable") + } + ) default: fatalError("Import \(importEntry) not supported in explore mode yet") } - hostModuleStubs[importEntry.module] = hostModule } - // Instruction dumping requires token threading model for now - let configuration = RuntimeConfiguration(threadingModel: .token) - let runtime = Runtime(hostModules: hostModuleStubs, configuration: configuration) - let instance = try runtime.instantiate(module: module) + let instance = try module.instantiate(store: store, imports: imports) var stdout = Stdout() - try instance.dumpFunctions(to: &stdout, module: module, runtime: runtime) + try instance.dumpFunctions(to: &stdout, module: module) } } diff --git a/Sources/CLI/Commands/Run.swift b/Sources/CLI/Commands/Run.swift index cd4f2f1a..f5d4f0ef 100644 --- a/Sources/CLI/Commands/Run.swift +++ b/Sources/CLI/Commands/Run.swift @@ -97,8 +97,8 @@ struct Run: ParsableCommand { } /// Derives the runtime interceptor based on the command line arguments - func deriveInterceptor() throws -> (interceptor: RuntimeInterceptor?, finalize: () -> Void) { - var interceptors: [RuntimeInterceptor] = [] + func deriveInterceptor() throws -> (interceptor: EngineInterceptor?, finalize: () -> Void) { + var interceptors: [EngineInterceptor] = [] var finalizers: [() -> Void] = [] if self.signpost { @@ -131,7 +131,7 @@ struct Run: ParsableCommand { return (MultiplexingInterceptor(interceptors), { finalizers.forEach { $0() } }) } - private func deriveSignpostTracer() -> RuntimeInterceptor? { + private func deriveSignpostTracer() -> EngineInterceptor? { #if canImport(os.signpost) if #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) { let signposter = SignpostTracer(signposter: OSSignposter()) @@ -142,17 +142,17 @@ struct Run: ParsableCommand { return nil } - private func deriveRuntimeConfiguration() -> RuntimeConfiguration { - let threadingModel: RuntimeConfiguration.ThreadingModel? + private func deriveRuntimeConfiguration() -> EngineConfiguration { + let threadingModel: EngineConfiguration.ThreadingModel? switch self.threadingModel { case .direct: threadingModel = .direct case .token: threadingModel = .token case nil: threadingModel = nil } - return RuntimeConfiguration(threadingModel: threadingModel) + return EngineConfiguration(threadingModel: threadingModel) } - func instantiateWASI(module: Module, interceptor: RuntimeInterceptor?) throws -> () throws -> Void { + func instantiateWASI(module: Module, interceptor: EngineInterceptor?) throws -> () throws -> Void { // Flatten environment variables into a dictionary (Respect the last value if a key is duplicated) let environment = environment.reduce(into: [String: String]()) { $0[$1.key] = $1.value @@ -161,15 +161,18 @@ struct Run: ParsableCommand { $0[$1] = $1 } let wasi = try WASIBridgeToHost(args: [path] + arguments, environment: environment, preopens: preopens) - let runtime = Runtime(hostModules: wasi.hostModules, interceptor: interceptor, configuration: deriveRuntimeConfiguration()) - let moduleInstance = try runtime.instantiate(module: module) + let engine = Engine(configuration: deriveRuntimeConfiguration(), interceptor: interceptor) + let store = Store(engine: engine) + var imports = Imports() + wasi.link(to: &imports, store: store) + let moduleInstance = try module.instantiate(store: store, imports: imports) return { - let exitCode = try wasi.start(moduleInstance, runtime: runtime) + let exitCode = try wasi.start(moduleInstance) throw ExitCode(Int32(exitCode)) } } - func instantiateNonWASI(module: Module, interceptor: RuntimeInterceptor?) throws -> (() throws -> Void)? { + func instantiateNonWASI(module: Module, interceptor: EngineInterceptor?) throws -> (() throws -> Void)? { let functionName = arguments.first let arguments = arguments.dropFirst() @@ -192,11 +195,16 @@ struct Run: ParsableCommand { return nil } - let runtime = Runtime(interceptor: interceptor, configuration: deriveRuntimeConfiguration()) - let moduleInstance = try runtime.instantiate(module: module) + let engine = Engine(configuration: deriveRuntimeConfiguration(), interceptor: interceptor) + let store = Store(engine: engine) + let instance = try module.instantiate(store: store) return { log("Started invoking function \"\(functionName)\" with parameters: \(parameters)", verbose: true) - let results = try runtime.invoke(moduleInstance, function: functionName, with: parameters) + guard let toInvoke = instance.exports[function: functionName] else { + log("Error: Function \"\(functionName)\" not found in the module.") + return + } + let results = try toInvoke.invoke(parameters) print(results.description) } } diff --git a/Sources/WITOverlayGenerator/HostGenerators/HostExportFunction.swift b/Sources/WITOverlayGenerator/HostGenerators/HostExportFunction.swift index 28a4f900..6c2041db 100644 --- a/Sources/WITOverlayGenerator/HostGenerators/HostExportFunction.swift +++ b/Sources/WITOverlayGenerator/HostGenerators/HostExportFunction.swift @@ -252,13 +252,12 @@ struct HostExportFunction { ) var signature = try signatureTranslation.signature(function: function, name: ConvertCase.camelCase(kebab: name.apiSwiftName)) let witParameters = signature.parameters.map(\.label) - signature.parameters.insert(("runtime", "Runtime"), at: 0) signature.hasThrows = true printer.write(line: signature.description + " {") try printer.indent { let optionsVar = builder.variable("options") printer.write(line: "let \(optionsVar) = CanonicalOptions._derive(from: instance, exportName: \"\(name.abiName)\")") - printer.write(line: "let \(context.contextVar) = CanonicalCallContext(options: \(optionsVar), instance: instance, runtime: runtime)") + printer.write(line: "let \(context.contextVar) = CanonicalCallContext(options: \(optionsVar), instance: instance)") // Suppress unused variable warning for "context" printer.write(line: "_ = \(context.contextVar)") @@ -266,9 +265,15 @@ struct HostExportFunction { parameterNames: witParameters, coreSignature: coreSignature, typeResolver: typeResolver, printer: printer ) - var call = "try runtime.invoke(instance, function: \"\(name.abiName)\"" + let functionVar = builder.variable("function") + printer.write(multiline: """ + guard let \(functionVar) = instance.exports[function: \"\(name.abiName)\"] else { + throw CanonicalABIError(description: "Function \\"\(name.abiName)\\" not found in the instance") + } + """) + var call = "try \(functionVar)(" if !arguments.isEmpty { - call += ", with: [\(arguments.map(\.description).joined(separator: ", "))]" + call += "[\(arguments.map(\.description).joined(separator: ", "))]" } call += ")" if coreSignature.isIndirectResult { diff --git a/Sources/WITOverlayGenerator/HostGenerators/HostWorldGenerator.swift b/Sources/WITOverlayGenerator/HostGenerators/HostWorldGenerator.swift index 7ab4551a..2c32d967 100644 --- a/Sources/WITOverlayGenerator/HostGenerators/HostWorldGenerator.swift +++ b/Sources/WITOverlayGenerator/HostGenerators/HostWorldGenerator.swift @@ -23,7 +23,7 @@ struct HostWorldGenerator: ASTVisitor { struct \(ConvertCase.pascalCase(world.name)) { let instance: WasmKit.Instance - static func link(_ hostModules: inout [String: HostModule]) { + static func link(to imports: inout Imports, store: Store) { } """) // Enter world struct body diff --git a/Sources/WasmKit/CMakeLists.txt b/Sources/WasmKit/CMakeLists.txt index 957e6bad..f7594f06 100644 --- a/Sources/WasmKit/CMakeLists.txt +++ b/Sources/WasmKit/CMakeLists.txt @@ -1,4 +1,6 @@ add_wasmkit_library(WasmKit + Engine.swift + Imports.swift Module.swift ModuleParser.swift Translator.swift @@ -14,7 +16,9 @@ add_wasmkit_library(WasmKit Execution/Instructions/Memory.swift Execution/Instructions/Misc.swift Execution/Instructions/InstructionSupport.swift + Execution/ConstEvaluation.swift Execution/DispatchInstruction.swift + Execution/EngineInterceptor.swift Execution/Errors.swift Execution/Execution.swift Execution/Function.swift @@ -23,7 +27,6 @@ add_wasmkit_library(WasmKit Execution/NameRegistry.swift Execution/Profiler.swift Execution/Runtime.swift - Execution/RuntimeInterceptor.swift Execution/SignpostTracer.swift Execution/Store.swift Execution/StoreAllocator.swift diff --git a/Sources/WasmKit/Component/CanonicalCall.swift b/Sources/WasmKit/Component/CanonicalCall.swift index 7abbec54..847a44d8 100644 --- a/Sources/WasmKit/Component/CanonicalCall.swift +++ b/Sources/WasmKit/Component/CanonicalCall.swift @@ -1,7 +1,13 @@ @_exported import WasmTypes -struct CanonicalABIError: Error, CustomStringConvertible { - let description: String +/// Error type for canonical ABI operations. +public struct CanonicalABIError: Error, CustomStringConvertible { + public let description: String + + @_documentation(visibility: internal) + public init(description: String) { + self.description = description + } } /// Call context for `(canon lift)` or `(canon lower)` operations. @@ -14,17 +20,14 @@ public struct CanonicalCallContext { public let options: CanonicalOptions /// The module instance that defines the lift/lower operation. public let instance: Instance - /// The executing `Runtime` instance - public let runtime: Runtime /// A reference to the guest memory. public var guestMemory: Memory { options.memory } - public init(options: CanonicalOptions, instance: Instance, runtime: Runtime) { + public init(options: CanonicalOptions, instance: Instance) { self.options = options self.instance = instance - self.runtime = runtime } /// Call `cabi_realloc` export with the given arguments. @@ -37,8 +40,8 @@ public struct CanonicalCallContext { guard let realloc = options.realloc else { throw CanonicalABIError(description: "Missing required \"cabi_realloc\" export") } - let results = try realloc.invoke( - [.i32(old), .i32(oldSize), .i32(oldAlign), .i32(newSize)], runtime: runtime + let results = try realloc( + [.i32(old), .i32(oldSize), .i32(oldAlign), .i32(newSize)] ) guard results.count == 1 else { throw CanonicalABIError(description: "\"cabi_realloc\" export should return a single value") @@ -56,9 +59,9 @@ extension CanonicalCallContext { return instance } - @available(*, deprecated, renamed: "init(options:instance:runtime:)") + @available(*, deprecated, renamed: "init(options:instance:)") public init(options: CanonicalOptions, moduleInstance: Instance, runtime: Runtime) { - self.init(options: options, instance: moduleInstance, runtime: runtime) + self.init(options: options, instance: moduleInstance) } } diff --git a/Sources/WasmKit/Component/CanonicalOptions.swift b/Sources/WasmKit/Component/CanonicalOptions.swift index b5fb949e..de9a562f 100644 --- a/Sources/WasmKit/Component/CanonicalOptions.swift +++ b/Sources/WasmKit/Component/CanonicalOptions.swift @@ -35,13 +35,13 @@ public struct CanonicalOptions { /// FIXME: This deriviation is wrong because the options should be determined by `(canon lift)` or `(canon lower)` /// in an encoded component at componetizing-time. (e.g. wit-component tool is one of the componetizers) /// Remove this temporary method after we will accept binary form of component file. - public static func _derive(from moduleInstance: Instance, exportName: String) -> CanonicalOptions { - guard case let .memory(memory) = moduleInstance.exports["memory"] else { + public static func _derive(from instance: Instance, exportName: String) -> CanonicalOptions { + guard case let .memory(memory) = instance.exports["memory"] else { fatalError("Missing required \"memory\" export") } return CanonicalOptions( memory: memory, stringEncoding: .utf8, - realloc: moduleInstance.exportedFunction(name: "cabi_realloc"), - postReturn: moduleInstance.exportedFunction(name: "cabi_post_\(exportName)")) + realloc: instance.exportedFunction(name: "cabi_realloc"), + postReturn: instance.exportedFunction(name: "cabi_post_\(exportName)")) } } diff --git a/Sources/WasmKit/Docs.docc/Docs.md b/Sources/WasmKit/Docs.docc/Docs.md index 4844df4b..900bdb8e 100644 --- a/Sources/WasmKit/Docs.docc/Docs.md +++ b/Sources/WasmKit/Docs.docc/Docs.md @@ -29,19 +29,27 @@ let module = try parseWasm( ) ) -// Define a host function that prints an i32 value. -let hostPrint = HostFunction(type: FunctionType(parameters: [.i32])) { _, args in - // This function is called from "print_add" in the WebAssembly module. - print(args[0]) - return [] -} -// Create a runtime importing the host function. -let runtime = Runtime(hostModules: [ - "printer": HostModule(functions: ["print_i32": hostPrint]) -]) -let instance = try runtime.instantiate(module: module) +// Create engine and store +let engine = Engine() +let store = Store(engine: engine) + +// Instantiate a parsed module with importing a host function +let instance = try module.instantiate( + store: store, + // Import a host function that prints an i32 value. + imports: [ + "printer": [ + "print_i32": Function(store: store, parameters: [.i32]) { _, args in + // This function is called from "print_add" in the WebAssembly module. + print(args[0]) + return [] + } + ] + ] +) // Invoke the exported function "print_add" -_ = try runtime.invoke(instance, function: "print_add", with: [.i32(42), .i32(3)]) +let printAdd = instance.exports[function: "print_add"]! +try printAdd([.i32(42), .i32(3)]) ``` See [examples](https://github.com/swiftwasm/WasmKit/tree/main/Examples) for executable examples. @@ -61,12 +69,16 @@ let module = try parseWasm(filePath: "wasm/hello.wasm") // Create a WASI instance forwarding to the host environment. let wasi = try WASIBridgeToHost() -// Create a runtime with WASI host modules. -let runtime = Runtime(hostModules: wasi.hostModules) -let instance = try runtime.instantiate(module: module) +// Create engine and store +let engine = Engine() +let store = Store(engine: engine) +// Instantiate a parsed module importing WASI +var imports = Imports() +wasi.link(to: &imports, store: store) +let instance = try module.instantiate(store: store, imports: imports) // Start the WASI command-line application. -let exitCode = try wasi.start(instance, runtime: runtime) +let exitCode = try wasi.start(instance) // Exit the Swift program with the WASI exit code. exit(Int32(exitCode)) ``` @@ -77,20 +89,26 @@ See [examples](https://github.com/swiftwasm/WasmKit/tree/main/Examples) for exec ### Basic Concepts +- ``Engine`` +- ``Store`` - ``Module`` - ``Instance`` -- ``Runtime`` -- ``Store`` +- ``Function`` ### Binary Parser - ``parseWasm(bytes:features:)`` - ``parseWasm(filePath:features:)`` +### Other WebAssembly Entities + +- ``Global`` +- ``Memory`` +- ``Table`` + ### Extending Runtime -- ``HostModule`` -- ``HostFunction`` +- ``Imports`` - ``Caller`` - ``GuestMemory`` - ``UnsafeGuestPointer`` diff --git a/Sources/WasmKit/Engine.swift b/Sources/WasmKit/Engine.swift new file mode 100644 index 00000000..bc77fe94 --- /dev/null +++ b/Sources/WasmKit/Engine.swift @@ -0,0 +1,102 @@ +import _CWasmKit.Platform + +/// A WebAssembly execution engine. +/// +/// An engine is responsible storing the configuration for the execution of +/// WebAssembly code such as interpreting mode, enabled features, etc. +/// Typically, you will need a single engine instance per application. +public final class Engine { + /// The engine configuration. + public let configuration: EngineConfiguration + let interceptor: EngineInterceptor? + let funcTypeInterner: Interner + + /// Create a new execution engine. + /// + /// - Parameters: + /// - configuration: The engine configuration. + /// - interceptor: An optional runtime interceptor to intercept execution of instructions. + public init(configuration: EngineConfiguration = EngineConfiguration(), interceptor: EngineInterceptor? = nil) { + self.configuration = configuration + self.interceptor = interceptor + self.funcTypeInterner = Interner() + } + + /// Migration aid for the old ``Runtime/instantiate(module:)`` + @available(*, unavailable, message: "Use ``Module/instantiate(store:imports:)`` instead") + public func instantiate(module: Module) -> Instance { fatalError() } +} + +/// The configuration for the WebAssembly execution engine. +public struct EngineConfiguration { + /// The threading model, which determines how to dispatch instruction + /// execution, to use for the virtual machine interpreter. + public enum ThreadingModel { + /// Direct threaded code + /// - Note: This is the default model for platforms that support + /// `musttail` calls. + case direct + /// Indirect threaded code + /// - Note: This is a fallback model for platforms that do not support + /// `musttail` calls. + case token + + static var useDirectThreadedCode: Bool { + return WASMKIT_USE_DIRECT_THREADED_CODE == 1 + } + + static var defaultForCurrentPlatform: ThreadingModel { + return useDirectThreadedCode ? .direct : .token + } + } + + /// The threading model to use for the virtual machine interpreter. + public var threadingModel: ThreadingModel + + /// Initializes a new instance of `EngineConfiguration`. + /// - Parameter threadingModel: The threading model to use for the virtual + /// machine interpreter. If `nil`, the default threading model for the + /// current platform will be used. + public init(threadingModel: ThreadingModel? = nil) { + self.threadingModel = threadingModel ?? .defaultForCurrentPlatform + } +} + +@_documentation(visibility: internal) +public protocol EngineInterceptor { + func onEnterFunction(_ function: Function) + func onExitFunction(_ function: Function) +} + +/// An interceptor that multiplexes multiple interceptors +@_documentation(visibility: internal) +public class MultiplexingInterceptor: EngineInterceptor { + private let interceptors: [EngineInterceptor] + + /// Creates a new multiplexing interceptor + /// - Parameter interceptors: The interceptors to multiplex + public init(_ interceptors: [EngineInterceptor]) { + self.interceptors = interceptors + } + + public func onEnterFunction(_ function: Function) { + for interceptor in interceptors { + interceptor.onEnterFunction(function) + } + } + + public func onExitFunction(_ function: Function) { + for interceptor in interceptors { + interceptor.onExitFunction(function) + } + } +} + +extension Engine { + func resolveType(_ type: InternedFuncType) -> FunctionType { + return funcTypeInterner.resolve(type) + } + func internType(_ type: FunctionType) -> InternedFuncType { + return funcTypeInterner.intern(type) + } +} diff --git a/Sources/WasmKit/Execution/ConstEvaluation.swift b/Sources/WasmKit/Execution/ConstEvaluation.swift new file mode 100644 index 00000000..7b022fd1 --- /dev/null +++ b/Sources/WasmKit/Execution/ConstEvaluation.swift @@ -0,0 +1,80 @@ +import WasmParser + +protocol ConstEvaluationContextProtocol { + func functionRef(_ index: FunctionIndex) throws -> Reference + func globalValue(_ index: GlobalIndex) throws -> Value +} + +extension InternalInstance: ConstEvaluationContextProtocol { + func functionRef(_ index: FunctionIndex) throws -> Reference { + return try .function(from: self.functions[validating: Int(index)]) + } + func globalValue(_ index: GlobalIndex) throws -> Value { + return try self.globals[validating: Int(index)].value + } +} + +struct ConstEvaluationContext: ConstEvaluationContextProtocol { + let functions: ImmutableArray + var globals: [Value] + func functionRef(_ index: FunctionIndex) throws -> Reference { + return try .function(from: self.functions[validating: Int(index)]) + } + func globalValue(_ index: GlobalIndex) throws -> Value { + guard index < globals.count else { + throw GlobalEntity.createOutOfBoundsError(index: Int(index), count: globals.count) + } + return self.globals[Int(index)] + } +} + +extension ConstExpression { + func evaluate(context: C) throws -> Value { + guard self.last == .end, self.count == 2 else { + throw InstantiationError.unsupported("Expect `end` at the end of offset expression") + } + let constInst = self[0] + switch constInst { + case .i32Const(let value): return .i32(UInt32(bitPattern: value)) + case .i64Const(let value): return .i64(UInt64(bitPattern: value)) + case .f32Const(let value): return .f32(value.bitPattern) + case .f64Const(let value): return .f64(value.bitPattern) + case .globalGet(let globalIndex): + return try context.globalValue(globalIndex) + case .refNull(let type): + switch type { + case .externRef: return .ref(.extern(nil)) + case .funcRef: return .ref(.function(nil)) + } + case .refFunc(let functionIndex): + return try .ref(context.functionRef(functionIndex)) + default: + throw InstantiationError.unsupported("illegal const expression instruction: \(constInst)") + } + } +} + +extension WasmParser.ElementSegment { + func evaluateInits(context: C) throws -> [Reference] { + try self.initializer.map { expression -> Reference in + switch expression[0] { + case let .refFunc(index): + return try context.functionRef(index) + case .refNull(.funcRef): + return .function(nil) + case .refNull(.externRef): + return .extern(nil) + case .globalGet(let index): + let value = try context.globalValue(index) + switch value { + case .ref(.function(let addr)): + return .function(addr) + default: + throw Trap._raw("Unexpected global value type for element initializer expression") + } + default: + throw Trap._raw("Unexpected element initializer expression: \(expression)") + } + } + } +} diff --git a/Sources/WasmKit/Execution/EngineInterceptor.swift b/Sources/WasmKit/Execution/EngineInterceptor.swift new file mode 100644 index 00000000..e69de29b diff --git a/Sources/WasmKit/Execution/Errors.swift b/Sources/WasmKit/Execution/Errors.swift index 60638e96..29405ad9 100644 --- a/Sources/WasmKit/Execution/Errors.swift +++ b/Sources/WasmKit/Execution/Errors.swift @@ -95,6 +95,7 @@ public enum InstantiationError: Error { public enum ImportError: Error { case unknownImport(moduleName: String, externalName: String) case incompatibleImportType + case importedEntityFromDifferentStore case moduleInstanceAlreadyRegistered(String) /// Human-readable text representation of the trap that `.wast` text format expects in assertions @@ -104,6 +105,8 @@ public enum ImportError: Error { return "unknown import" case .incompatibleImportType: return "incompatible import type" + case .importedEntityFromDifferentStore: + return "imported entity from different store" case let .moduleInstanceAlreadyRegistered(name): return "a module instance is already registered under a name `\(name)" } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 099dd13e..cec7c752 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -5,8 +5,8 @@ import _CWasmKit /// Each new invocation through exported function has a separate ``Execution`` /// even though the invocation happens during another invocation. struct Execution { - /// The reference to the ``Runtime`` associated with the execution. - let runtime: RuntimeRef + /// The reference to the ``Store`` associated with the execution. + let store: StoreRef /// The end of the VM stack space. private var stackEnd: UnsafeMutablePointer /// The error trap thrown during execution. @@ -15,9 +15,9 @@ struct Execution { private var trap: UnsafeRawPointer? = nil /// Executes the given closure with a new execution state associated with - /// the given ``Runtime`` instance. + /// the given ``Store`` instance. static func with( - runtime: RuntimeRef, + store: StoreRef, body: (inout Execution, Sp) throws -> T ) rethrows -> T { let limit = Int(UInt16.max) @@ -25,7 +25,7 @@ struct Execution { defer { valueStack.deallocate() } - var context = Execution(runtime: runtime, stackEnd: valueStack.advanced(by: limit)) + var context = Execution(store: store, stackEnd: valueStack.advanced(by: limit)) defer { if let trap = context.trap { // Manually release the error object because the trap is caught in C and @@ -81,20 +81,16 @@ struct Execution { } } -/// An unmanaged reference to a runtime. +/// An unmanaged reference to a ``Store`` instance. /// - Note: This is used to avoid ARC overhead during VM execution. -struct RuntimeRef { - private let _value: Unmanaged +struct StoreRef { + private let _value: Unmanaged - var value: Runtime { + var value: Store { _value.takeUnretainedValue() } - var store: Store { - value.store - } - - init(_ value: __shared Runtime) { + init(_ value: __shared Store) { self._value = .passUnretained(value) } } @@ -217,15 +213,15 @@ extension Pc { /// - Returns: The result values of the function. @inline(never) func executeWasm( - runtime: Runtime, + store: Store, function handle: InternalFunction, type: FunctionType, arguments: [Value], callerInstance: InternalInstance ) throws -> [Value] { // NOTE: `runtime` variable must not outlive this function - let runtime = RuntimeRef(runtime) - return try Execution.with(runtime: runtime) { (stack, sp) in + let store = StoreRef(store) + return try Execution.with(store: store) { (stack, sp) in // Advance the stack pointer to be able to reference negative indices // for saving slots. let sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) @@ -235,7 +231,7 @@ func executeWasm( try withUnsafeTemporaryAllocation(of: CodeSlot.self, capacity: 2) { rootISeq in rootISeq[0] = Instruction.endOfExecution.headSlot( - threadingModel: runtime.value.configuration.threadingModel + threadingModel: store.value.engine.configuration.threadingModel ) try stack.execute( sp: sp, @@ -314,7 +310,7 @@ extension Execution { sp: sp, pc: pc, md: &md, ms: &ms ) do { - switch self.runtime.value.configuration.threadingModel { + switch self.store.value.engine.configuration.threadingModel { case .direct: try runDirectThreaded(sp: sp, pc: pc, md: md, ms: ms) case .token: @@ -458,7 +454,7 @@ extension Execution { return (iseq.baseAddress, newSp) } else { let function = function.host - let resolvedType = runtime.value.resolveType(function.type) + let resolvedType = store.value.engine.resolveType(function.type) let layout = FrameHeaderLayout(type: resolvedType) let parameters = resolvedType.parameters.enumerated().map { (i, type) in sp[spAddend + layout.paramReg(i)].cast(to: type) @@ -466,7 +462,7 @@ extension Execution { let instance = self.currentInstance(sp: sp) let caller = Caller( instanceHandle: instance, - runtime: runtime.value + store: store.value ) let results = try function.implementation(caller, Array(parameters)) for (index, result) in results.enumerated() { diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 9635cd57..de5c7f1a 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -1,4 +1,5 @@ import WasmParser +import struct WasmTypes.FunctionType /// A WebAssembly guest function or host function. /// @@ -6,11 +7,67 @@ import WasmParser /// public struct Function: Equatable { internal let handle: InternalFunction - let allocator: StoreAllocator + let store: Store + + internal init(handle: InternalFunction, store: Store) { + self.handle = handle + self.store = store + } + + /// Creates a new function instance backed by a native host function. + /// + /// - Parameters: + /// - store: The store to allocate the function in. + /// - parameters: The types of the function parameters. + /// - results: The types of the function results. + /// - body: The implementation of the function. + public init( + store: Store, + parameters: [ValueType], results: [ValueType] = [], + body: @escaping (Caller, [Value]) throws -> [Value] + ) { + self.init(store: store, type: FunctionType(parameters: parameters, results: results), body: body) + } + + /// Creates a new function instance backed by a native host function. + /// + /// - Parameters: + /// - store: The store to allocate the function in. + /// - type: The signature type of the function. + /// - body: The implementation of the function. + public init( + store: Store, + type: FunctionType, + body: @escaping (Caller, [Value]) throws -> [Value] + ) { + self.init(handle: store.allocator.allocate(type: type, implementation: body, engine: store.engine), store: store) + } /// The signature type of the function. public var type: FunctionType { - allocator.funcTypeInterner.resolve(handle.type) + store.allocator.funcTypeInterner.resolve(handle.type) + } + + /// Invokes a function of the given address with the given parameters. + /// + /// - Parameters: + /// - arguments: The arguments to pass to the function. + /// - Throws: A trap if the function invocation fails. + /// - Returns: The results of the function invocation. + @discardableResult + public func invoke(_ arguments: [Value] = []) throws -> [Value] { + return try handle.invoke(arguments, store: store) + } + + /// Invokes a function of the given address with the given parameters. + /// + /// - Parameter + /// - arguments: The arguments to pass to the function. + /// - Throws: A trap if the function invocation fails. + /// - Returns: The results of the function invocation. + @discardableResult + public func callAsFunction(_ arguments: [Value] = []) throws -> [Value] { + return try invoke(arguments) } /// Invokes a function of the given address with the given parameters. @@ -20,9 +77,10 @@ public struct Function: Equatable { /// - runtime: The runtime to use for the function invocation. /// - Throws: A trap if the function invocation fails. /// - Returns: The results of the function invocation. + @available(*, deprecated, renamed: "invoke(_:)") + @discardableResult public func invoke(_ arguments: [Value] = [], runtime: Runtime) throws -> [Value] { - assert(allocator === runtime.store.allocator, "Function is not from the same store as the runtime") - return try handle.invoke(arguments, runtime: runtime) + return try invoke(arguments) } } @@ -75,13 +133,13 @@ extension InternalFunction: ValidatableEntity { } extension InternalFunction { - func invoke(_ arguments: [Value], runtime: Runtime) throws -> [Value] { + func invoke(_ arguments: [Value], store: Store) throws -> [Value] { if isWasm { let entity = wasm - let resolvedType = runtime.resolveType(entity.type) + let resolvedType = store.engine.resolveType(entity.type) try check(functionType: resolvedType, parameters: arguments) return try executeWasm( - runtime: runtime, + store: store, function: self, type: resolvedType, arguments: arguments, @@ -89,9 +147,9 @@ extension InternalFunction { ) } else { let entity = host - let resolvedType = runtime.resolveType(entity.type) + let resolvedType = store.engine.resolveType(entity.type) try check(functionType: resolvedType, parameters: arguments) - let caller = Caller(instanceHandle: nil, runtime: runtime) + let caller = Caller(instanceHandle: nil, store: store) let results = try entity.implementation(caller, arguments) try check(functionType: resolvedType, results: results) return results @@ -124,12 +182,12 @@ extension InternalFunction { } @inline(never) - func ensureCompiled(runtime: RuntimeRef) throws { + func ensureCompiled(store: StoreRef) throws { let entity = self.wasm switch entity.code { case .uncompiled(let code): try entity.withValue { - let iseq = try $0.compile(runtime: runtime, code: code) + let iseq = try $0.compile(store: store, code: code) $0.code = .compiled(iseq) } case .compiled: break @@ -166,31 +224,33 @@ struct WasmFunctionEntity { } mutating func ensureCompiled(context: inout Execution) throws -> InstructionSequence { - try ensureCompiled(runtime: context.runtime) + try ensureCompiled(store: context.store) } - mutating func ensureCompiled(runtime: RuntimeRef) throws -> InstructionSequence { + mutating func ensureCompiled(store: StoreRef) throws -> InstructionSequence { switch code { case .uncompiled(let code): - return try compile(runtime: runtime, code: code) + return try compile(store: store, code: code) case .compiled(let iseq): return iseq } } @inline(never) - mutating func compile(runtime: RuntimeRef, code: InternalUncompiledCode) throws -> InstructionSequence { + mutating func compile(store: StoreRef, code: InternalUncompiledCode) throws -> InstructionSequence { + let store = store.value + let engine = store.engine let type = self.type var translator = try InstructionTranslator( - allocator: runtime.value.store.allocator.iseqAllocator, - runtimeConfiguration: runtime.value.configuration, - funcTypeInterner: runtime.value.funcTypeInterner, + allocator: store.allocator.iseqAllocator, + engineConfiguration: engine.configuration, + funcTypeInterner: engine.funcTypeInterner, module: instance, - type: runtime.value.resolveType(type), + type: engine.resolveType(type), locals: code.locals, functionIndex: index, codeSize: code.expression.count, - intercepting: runtime.value.interceptor != nil + intercepting: engine.interceptor != nil ) let iseq = try code.withValue { code in try translator.translate(code: code, instance: instance) diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index e939b279..f12aad6a 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -78,21 +78,96 @@ struct InstanceEntity /* : ~Copyable */ { var exports: [String: InternalExternalValue] var features: WasmFeatureSet var hasDataCount: Bool + + static var empty: InstanceEntity { + InstanceEntity( + types: [], + functions: ImmutableArray(), + tables: ImmutableArray(), + memories: ImmutableArray(), + globals: ImmutableArray(), + elementSegments: ImmutableArray(), + dataSegments: ImmutableArray(), + exports: [:], + features: [], + hasDataCount: false + ) + } } typealias InternalInstance = EntityHandle +/// A map of exported entities by name. +public struct Exports: Sequence { + let store: Store + let items: [String: InternalExternalValue] + + /// A collection of exported entities without their names. + public var values: [ExternalValue] { + self.map { $0.value } + } + + /// Returns the exported entity with the given name. + public subscript(_ name: String) -> ExternalValue? { + guard let entity = items[name] else { return nil } + return ExternalValue(handle: entity, store: store) + } + + /// Returns the exported function with the given name. + public subscript(function name: String) -> Function? { + guard case .function(let function) = self[name] else { return nil } + return function + } + + /// Returns the exported table with the given name. + public subscript(table name: String) -> Table? { + guard case .table(let table) = self[name] else { return nil } + return table + } + + /// Returns the exported memory with the given name. + public subscript(memory name: String) -> Memory? { + guard case .memory(let memory) = self[name] else { return nil } + return memory + } + + /// Returns the exported global with the given name. + public subscript(global name: String) -> Global? { + guard case .global(let global) = self[name] else { return nil } + return global + } + + public struct Iterator: IteratorProtocol { + private let store: Store + private var iterator: Dictionary.Iterator + + init(parent: Exports) { + self.store = parent.store + self.iterator = parent.items.makeIterator() + } + + public mutating func next() -> (name: String, value: ExternalValue)? { + guard let (name, entity) = iterator.next() else { return nil } + return (name, ExternalValue(handle: entity, store: store)) + } + } + + public func makeIterator() -> Iterator { + Iterator(parent: self) + } +} + /// A stateful instance of a WebAssembly module. -/// Usually instantiated by ``Runtime/instantiate(module:)``. +/// Usually instantiated by ``Module/instantiate(store:imports:)``. /// > Note: /// public struct Instance { let handle: InternalInstance - let allocator: StoreAllocator + let store: Store - init(handle: InternalInstance, allocator: StoreAllocator) { + init(handle: InternalInstance, store: Store) { self.handle = handle - self.allocator = allocator + self.store = store } /// Finds an exported entity by name. @@ -101,7 +176,7 @@ public struct Instance { /// - Returns: The exported entity if found, otherwise `nil`. public func export(_ name: String) -> ExternalValue? { guard let entity = handle.exports[name] else { return nil } - return ExternalValue(handle: entity, allocator: allocator) + return ExternalValue(handle: entity, store: store) } /// Finds an exported function by name. @@ -113,35 +188,33 @@ public struct Instance { return function } - public typealias Exports = [String: ExternalValue] - /// A dictionary of exported entities by name. public var exports: Exports { - handle.exports.mapValues { ExternalValue(handle: $0, allocator: allocator) } + Exports(store: store, items: handle.exports) } /// Dumps the textual representation of all functions in the instance. /// /// - Precondition: The instance must be compiled with the token threading model. @_spi(OnlyForCLI) - public func dumpFunctions(to target: inout Target, module: Module, runtime: Runtime) throws where Target: TextOutputStream { + public func dumpFunctions(to target: inout Target, module: Module) throws where Target: TextOutputStream { for (offset, function) in self.handle.functions.enumerated() { let index = offset guard function.isWasm else { continue } target.write("==== Function[\(index)]") - if let name = try? runtime.store.nameRegistry.lookup(function) { + if let name = try? store.nameRegistry.lookup(function) { target.write(" '\(name)'") } target.write(" ====\n") guard case .uncompiled(let code) = function.wasm.code else { fatalError("Already compiled!?") } - try function.ensureCompiled(runtime: RuntimeRef(runtime)) + try function.ensureCompiled(store: StoreRef(store)) let (iseq, locals, _) = function.assumeCompiled() // Print slot space information let stackLayout = try StackLayout( - type: runtime.funcTypeInterner.resolve(function.type), + type: store.engine.funcTypeInterner.resolve(function.type), numberOfLocals: locals, codeSize: code.expression.count ) @@ -149,8 +222,8 @@ public struct Instance { var context = InstructionPrintingContext( shouldColor: true, - function: Function(handle: function, allocator: allocator), - nameRegistry: runtime.store.nameRegistry + function: Function(handle: function, store: store), + nameRegistry: store.nameRegistry ) iseq.write(to: &target, context: &context) } @@ -252,6 +325,30 @@ typealias InternalTable = EntityHandle public struct Table: Equatable { let handle: InternalTable let allocator: StoreAllocator + + init(handle: InternalTable, allocator: StoreAllocator) { + self.handle = handle + self.allocator = allocator + } + + /// Creates a new table instance with the given type. + public init(store: Store, type: TableType) throws { + self.init( + handle: try store.allocator.allocate(tableType: type, resourceLimiter: store.resourceLimiter), + allocator: store.allocator + ) + } + + /// The type of the table instance. + public var type: TableType { + handle.tableType + } + + /// Accesses the element at the given index. + public subscript(index: Int) -> Reference { + get { handle.elements[index] } + nonmutating set { handle.withValue { $0.elements[index] = newValue } } + } } struct MemoryEntity /* : ~Copyable */ { @@ -314,10 +411,28 @@ public struct Memory: Equatable { let handle: InternalMemory let allocator: StoreAllocator + init(handle: InternalMemory, allocator: StoreAllocator) { + self.handle = handle + self.allocator = allocator + } + + /// Creates a new memory instance with the given type. + public init(store: Store, type: MemoryType) throws { + self.init( + handle: try store.allocator.allocate(memoryType: type, resourceLimiter: store.resourceLimiter), + allocator: store.allocator + ) + } + /// Returns a copy of the memory data. public var data: [UInt8] { handle.data } + + /// The type of the memory instance. + public var type: MemoryType { + handle.limit + } } extension Memory: GuestMemory { @@ -443,16 +558,29 @@ public enum ExternalValue: Equatable { case memory(Memory) case global(Global) - init(handle: InternalExternalValue, allocator: StoreAllocator) { + init(handle: InternalExternalValue, store: Store) { switch handle { case .function(let function): - self = .function(Function(handle: function, allocator: allocator)) + self = .function(Function(handle: function, store: store)) + case .table(let table): + self = .table(Table(handle: table, allocator: store.allocator)) + case .memory(let memory): + self = .memory(Memory(handle: memory, allocator: store.allocator)) + case .global(let global): + self = .global(Global(handle: global, allocator: store.allocator)) + } + } + + func internalize() -> (InternalExternalValue, StoreAllocator) { + switch self { + case .function(let function): + return (.function(function.handle), function.store.allocator) case .table(let table): - self = .table(Table(handle: table, allocator: allocator)) + return (.table(table.handle), table.allocator) case .memory(let memory): - self = .memory(Memory(handle: memory, allocator: allocator)) + return (.memory(memory.handle), memory.allocator) case .global(let global): - self = .global(Global(handle: global, allocator: allocator)) + return (.global(global.handle), global.allocator) } } } diff --git a/Sources/WasmKit/Execution/Instructions/Control.swift b/Sources/WasmKit/Execution/Instructions/Control.swift index a0af1f85..d7929f72 100644 --- a/Sources/WasmKit/Execution/Instructions/Control.swift +++ b/Sources/WasmKit/Execution/Instructions/Control.swift @@ -102,9 +102,9 @@ extension Execution { // NOTE: `CompilingCallOperand` consumes 2 slots, discriminator is at -3 let headSlotPc = pc.advanced(by: -3) let callee = immediate.callee - try callee.ensureCompiled(runtime: runtime) + try callee.ensureCompiled(store: store) let replaced = Instruction.internalCall(immediate) - headSlotPc.pointee = replaced.headSlot(threadingModel: runtime.value.configuration.threadingModel) + headSlotPc.pointee = replaced.headSlot(threadingModel: store.value.engine.configuration.threadingModel) try _internalCall(sp: &sp, pc: &pc, callee: callee, internalCallOperand: immediate) return pc.next() } @@ -128,8 +128,8 @@ extension Execution { let function = InternalFunction(bitPattern: rawBitPattern) guard function.type == expectedType else { throw Trap.callIndirectFunctionTypeMismatch( - actual: runtime.value.resolveType(function.type), - expected: runtime.value.resolveType(expectedType) + actual: store.value.engine.resolveType(function.type), + expected: store.value.engine.resolveType(expectedType) ) } return (function, callerInstance) @@ -153,16 +153,14 @@ extension Execution { mutating func onEnter(sp: Sp, immediate: Instruction.OnEnterOperand) { let function = currentInstance(sp: sp).functions[Int(immediate)] - self.runtime.value.interceptor?.onEnterFunction( - Function(handle: function, allocator: self.runtime.store.allocator), - store: self.runtime.store + self.store.value.engine.interceptor?.onEnterFunction( + Function(handle: function, store: store.value) ) } mutating func onExit(sp: Sp, immediate: Instruction.OnExitOperand) { let function = currentInstance(sp: sp).functions[Int(immediate)] - self.runtime.value.interceptor?.onExitFunction( - Function(handle: function, allocator: self.runtime.store.allocator), - store: self.runtime.store + self.store.value.engine.interceptor?.onExitFunction( + Function(handle: function, store: store.value) ) } } diff --git a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift index 0741a48e..945ccc33 100644 --- a/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift +++ b/Sources/WasmKit/Execution/Instructions/InstructionSupport.swift @@ -189,7 +189,7 @@ extension UntypedValue { typealias OpcodeID = UInt64 extension Instruction { - func headSlot(threadingModel: RuntimeConfiguration.ThreadingModel) -> CodeSlot { + func headSlot(threadingModel: EngineConfiguration.ThreadingModel) -> CodeSlot { switch threadingModel { case .direct: return CodeSlot(handler) diff --git a/Sources/WasmKit/Execution/Instructions/Memory.swift b/Sources/WasmKit/Execution/Instructions/Memory.swift index 734f5065..72c82cfa 100644 --- a/Sources/WasmKit/Execution/Instructions/Memory.swift +++ b/Sources/WasmKit/Execution/Instructions/Memory.swift @@ -50,7 +50,7 @@ extension Execution { let value = sp[immediate.delta] let pageCount: UInt64 = isMemory64 ? value.i64 : UInt64(value.i32) - let oldPageCount = try memory.grow(by: Int(pageCount), resourceLimiter: runtime.store.resourceLimiter) + let oldPageCount = try memory.grow(by: Int(pageCount), resourceLimiter: store.value.resourceLimiter) CurrentMemory.assign(md: &md, ms: &ms, memory: &memory) sp[immediate.result] = UntypedValue(oldPageCount) } diff --git a/Sources/WasmKit/Execution/Instructions/Table.swift b/Sources/WasmKit/Execution/Instructions/Table.swift index 1d2595d4..0da97bf4 100644 --- a/Sources/WasmKit/Execution/Instructions/Table.swift +++ b/Sources/WasmKit/Execution/Instructions/Table.swift @@ -4,8 +4,7 @@ import WasmParser extension Execution { mutating func tableGet(sp: Sp, immediate: Instruction.TableGetOperand) throws { - let runtime = runtime.value - let table = getTable(immediate.tableIndex, sp: sp, store: runtime.store) + let table = getTable(immediate.tableIndex, sp: sp, store: store.value) let elementIndex = try getElementIndex(sp: sp, VReg(immediate.index), table) @@ -13,36 +12,32 @@ extension Execution { sp[immediate.result] = UntypedValue(.ref(reference)) } mutating func tableSet(sp: Sp, immediate: Instruction.TableSetOperand) throws { - let runtime = runtime.value - let table = getTable(immediate.tableIndex, sp: sp, store: runtime.store) + let table = getTable(immediate.tableIndex, sp: sp, store: store.value) let reference = sp.getReference(VReg(immediate.value), type: table.tableType) let elementIndex = try getElementIndex(sp: sp, VReg(immediate.index), table) setTableElement(table: table, Int(elementIndex), reference) } mutating func tableSize(sp: Sp, immediate: Instruction.TableSizeOperand) { - let runtime = runtime.value - let table = getTable(immediate.tableIndex, sp: sp, store: runtime.store) + let table = getTable(immediate.tableIndex, sp: sp, store: store.value) let elementsCount = table.elements.count sp[immediate.result] = UntypedValue(table.limits.isMemory64 ? .i64(UInt64(elementsCount)) : .i32(UInt32(elementsCount))) } mutating func tableGrow(sp: Sp, immediate: Instruction.TableGrowOperand) throws { - let runtime = runtime.value - let table = getTable(immediate.tableIndex, sp: sp, store: runtime.store) + let table = getTable(immediate.tableIndex, sp: sp, store: store.value) let growthSize = sp[immediate.delta].asAddressOffset(table.limits.isMemory64) let growthValue = sp.getReference(VReg(immediate.value), type: table.tableType) let oldSize = table.elements.count - guard try table.withValue({ try $0.grow(by: growthSize, value: growthValue, resourceLimiter: runtime.store.resourceLimiter) }) else { + guard try table.withValue({ try $0.grow(by: growthSize, value: growthValue, resourceLimiter: store.value.resourceLimiter) }) else { sp[immediate.result] = UntypedValue(.i32(Int32(-1).unsigned)) return } sp[immediate.result] = UntypedValue(table.limits.isMemory64 ? .i64(UInt64(oldSize)) : .i32(UInt32(oldSize))) } mutating func tableFill(sp: Sp, immediate: Instruction.TableFillOperand) throws { - let runtime = runtime.value - let table = getTable(immediate.tableIndex, sp: sp, store: runtime.store) + let table = getTable(immediate.tableIndex, sp: sp, store: store.value) let fillCounter = sp[immediate.size].asAddressOffset(table.limits.isMemory64) let fillValue = sp.getReference(immediate.value, type: table.tableType) let startIndex = sp[immediate.destOffset].asAddressOffset(table.limits.isMemory64) @@ -62,9 +57,9 @@ extension Execution { mutating func tableCopy(sp: Sp, immediate: Instruction.TableCopyOperand) throws { let sourceTableIndex = immediate.sourceIndex let destinationTableIndex = immediate.destIndex - let runtime = runtime.value - let sourceTable = getTable(sourceTableIndex, sp: sp, store: runtime.store) - let destinationTable = getTable(destinationTableIndex, sp: sp, store: runtime.store) + let store = self.store.value + let sourceTable = getTable(sourceTableIndex, sp: sp, store: store) + let destinationTable = getTable(destinationTableIndex, sp: sp, store: store) let copyCounter = sp[immediate.size].asAddressOffset( sourceTable.limits.isMemory64 || destinationTable.limits.isMemory64 @@ -100,7 +95,7 @@ extension Execution { mutating func tableInit(sp: Sp, immediate: Instruction.TableInitOperand) throws { let tableIndex = immediate.tableIndex let segmentIndex = immediate.segmentIndex - let destinationTable = getTable(tableIndex, sp: sp, store: runtime.store) + let destinationTable = getTable(tableIndex, sp: sp, store: store.value) let sourceElement = currentInstance(sp: sp).elementSegments[Int(segmentIndex)] let copyCounter = UInt64(sp[immediate.size].i32) diff --git a/Sources/WasmKit/Execution/Profiler.swift b/Sources/WasmKit/Execution/Profiler.swift index 6ca081b3..46a6cbd0 100644 --- a/Sources/WasmKit/Execution/Profiler.swift +++ b/Sources/WasmKit/Execution/Profiler.swift @@ -4,7 +4,7 @@ import SystemPackage /// A simple time-profiler for guest process to emit `chrome://tracing` format /// This profiler works only when WasmKit is built with debug configuration (`swift build -c debug`) @_documentation(visibility: internal) -public class GuestTimeProfiler: RuntimeInterceptor { +public class GuestTimeProfiler: EngineInterceptor { struct Event: Codable { enum Phase: String, Codable { case begin = "B" @@ -82,19 +82,19 @@ public class GuestTimeProfiler: RuntimeInterceptor { } } - public func onEnterFunction(_ function: Function, store: Store) { + public func onEnterFunction(_ function: Function) { let event = Event( ph: .begin, pid: 1, - name: store.nameRegistry.symbolicate(function.handle), + name: function.store.nameRegistry.symbolicate(function.handle), ts: getDurationSinceStart() ) addEventLine(event) } - public func onExitFunction(_ function: Function, store: Store) { + public func onExitFunction(_ function: Function) { let event = Event( ph: .end, pid: 1, - name: store.nameRegistry.symbolicate(function.handle), + name: function.store.nameRegistry.symbolicate(function.handle), ts: getDurationSinceStart() ) addEventLine(event) diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 5d20986d..8d3710f8 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -1,12 +1,25 @@ import WasmParser -import _CWasmKit.Platform /// A container to manage execution state of one or more module instances. +@available(*, deprecated, message: "Use `Engine` instead") public final class Runtime { public let store: Store - let interceptor: RuntimeInterceptor? - let funcTypeInterner: Interner - let configuration: RuntimeConfiguration + let engine: Engine + var interceptor: EngineInterceptor? { + engine.interceptor + } + var funcTypeInterner: Interner { + engine.funcTypeInterner + } + var configuration: EngineConfiguration { + engine.configuration + } + + var hostFunctions: [HostFunction] = [] + private var hostGlobals: [Global] = [] + /// This property is separate from `registeredModuleInstances`, as host exports + /// won't have a corresponding module instance. + fileprivate var availableExports: [String: [String: ExternalValue]] = [:] /// Initializes a new instant of a WebAssembly interpreter runtime. /// - Parameter hostModules: Host module names mapped to their corresponding ``HostModule`` definitions. @@ -14,16 +27,14 @@ public final class Runtime { /// - Parameter configuration: An optional runtime configuration to customize the runtime behavior. public init( hostModules: [String: HostModule] = [:], - interceptor: RuntimeInterceptor? = nil, - configuration: RuntimeConfiguration = RuntimeConfiguration() + interceptor: EngineInterceptor? = nil, + configuration: EngineConfiguration = EngineConfiguration() ) { - self.funcTypeInterner = Interner() - store = Store(funcTypeInterner: funcTypeInterner) - self.interceptor = interceptor - self.configuration = configuration + self.engine = Engine(configuration: configuration, interceptor: interceptor) + store = Store(engine: engine) for (moduleName, hostModule) in hostModules { - store.registerUniqueHostModule(hostModule, as: moduleName, runtime: self) + registerUniqueHostModule(hostModule, as: moduleName, engine: engine) } } @@ -33,147 +44,98 @@ public final class Runtime { func internType(_ type: FunctionType) -> InternedFuncType { return funcTypeInterner.intern(type) } -} - -public struct RuntimeConfiguration { - /// The threading model, which determines how to dispatch instruction - /// execution, to use for the virtual machine interpreter. - public enum ThreadingModel { - /// Direct threaded code - /// - Note: This is the default model for platforms that support - /// `musttail` calls. - case direct - /// Indirect threaded code - /// - Note: This is a fallback model for platforms that do not support - /// `musttail` calls. - case token - - static var useDirectThreadedCode: Bool { - return WASMKIT_USE_DIRECT_THREADED_CODE == 1 - } - static var defaultForCurrentPlatform: ThreadingModel { - return useDirectThreadedCode ? .direct : .token - } + public func instantiate(module: Module) throws -> Instance { + return try module.instantiate( + store: store, + imports: getExternalValues(module, runtime: self) + ) } - /// The threading model to use for the virtual machine interpreter. - public var threadingModel: ThreadingModel + /// Legacy compatibility method to register a module instance with a name. + public func register(_ instance: Instance, as name: String) throws { + guard availableExports[name] == nil else { + throw ImportError.moduleInstanceAlreadyRegistered(name) + } - /// Initializes a new instance of `RuntimeConfiguration`. - public init(threadingModel: ThreadingModel? = nil) { - self.threadingModel = threadingModel ?? .defaultForCurrentPlatform + availableExports[name] = Dictionary(uniqueKeysWithValues: instance.exports.map { ($0, $1) }) } -} -extension Runtime { - public func instantiate(module: Module) throws -> Instance { - let instance = try instantiate( - module: module, - externalValues: store.getExternalValues(module, runtime: self) - ) + /// Legacy compatibility method to register a host module with a name. + public func register(_ hostModule: HostModule, as name: String) throws { + guard availableExports[name] == nil else { + throw ImportError.moduleInstanceAlreadyRegistered(name) + } - return Instance(handle: instance, allocator: store.allocator) + registerUniqueHostModule(hostModule, as: name, engine: engine) } - /// > Note: - /// - func instantiate(module: Module, externalValues: [ExternalValue]) throws -> InternalInstance { - // Step 3 of instantiation algorithm, according to Wasm 2.0 spec. - guard module.imports.count == externalValues.count else { - throw InstantiationError.importsAndExternalValuesMismatch + /// Register the given host module assuming that the given name is not registered yet. + func registerUniqueHostModule(_ hostModule: HostModule, as name: String, engine: Engine) { + var moduleExports = [String: ExternalValue]() + + for (globalName, global) in hostModule.globals { + moduleExports[globalName] = .global(global) + hostGlobals.append(global) } - // Step 4. - let isValid = zip(module.imports, externalValues).map { i, e -> Bool in - switch (i.descriptor, e) { - case (.function, .function), - (.table, .table), - (.memory, .memory), - (.global, .global): - return true - default: return false - } - }.reduce(true) { $0 && $1 } + for (functionName, function) in hostModule.functions { + moduleExports[functionName] = .function( + Function( + handle: store.allocator.allocate(type: function.type, implementation: function.implementation, engine: engine), + store: store + ) + ) + hostFunctions.append(function) + } - guard isValid else { - throw InstantiationError.importsAndExternalValuesMismatch + for (memoryName, memoryAddr) in hostModule.memories { + moduleExports[memoryName] = .memory(memoryAddr) } - // Steps 5-8. + availableExports[name] = moduleExports + } - // Step 9. - // Process `elem.init` evaluation during allocation + func getExternalValues(_ module: Module, runtime: Runtime) throws -> Imports { + var result = Imports() - // Step 11. - let instance = try store.allocator.allocate( - module: module, runtime: self, - externalValues: externalValues - ) + for i in module.imports { + guard let moduleExports = availableExports[i.module], let external = moduleExports[i.name] else { + throw ImportError.unknownImport(moduleName: i.module, externalName: i.name) + } - if let nameSection = module.customSections.first(where: { $0.name == "name" }) { - // FIXME?: Just ignore parsing error of name section for now. - // Should emit warning instead of just discarding it? - try? store.nameRegistry.register(instance: instance, nameSection: nameSection) - } + switch (i.descriptor, external) { + case let (.function(typeIndex), .function(externalFunc)): + let type = externalFunc.handle.type + guard runtime.internType(module.types[Int(typeIndex)]) == type else { + throw ImportError.incompatibleImportType + } + result.define(i, external) - // Step 12-13. - - // Steps 14-15. - do { - for element in module.elements { - guard case let .active(tableIndex, offset) = element.mode else { continue } - let offsetValue = try offset.evaluate(context: instance) - let table = try instance.tables[validating: Int(tableIndex)] - try table.withValue { table in - guard let offset = offsetValue.maybeAddressOffset(table.limits.isMemory64) else { - throw InstantiationError.unsupported( - "Expect \(ValueType.addressType(isMemory64: table.limits.isMemory64)) offset of active element segment but got \(offsetValue)" - ) - } - let references = try element.evaluateInits(context: instance) - try table.initialize( - elements: references, from: 0, to: Int(offset), count: references.count - ) + case let (.table(tableType), .table(table)): + if let max = table.handle.limits.max, max < tableType.limits.min { + throw ImportError.incompatibleImportType } - } - } catch Trap.undefinedElement, Trap.tableSizeOverflow, Trap.outOfBoundsTableAccess { - throw InstantiationError.outOfBoundsTableAccess - } catch { - throw error - } + result.define(i, external) - // Step 16. - do { - for case let .active(data) in module.data { - let offsetValue = try data.offset.evaluate(context: instance) - let memory = try instance.memories[validating: Int(data.index)] - try memory.withValue { memory in - guard let offset = offsetValue.maybeAddressOffset(memory.limit.isMemory64) else { - throw InstantiationError.unsupported( - "Expect \(ValueType.addressType(isMemory64: memory.limit.isMemory64)) offset of active data segment but got \(offsetValue)" - ) - } - try memory.write(offset: Int(offset), bytes: data.initializer) + case let (.memory(memoryType), .memory(memory)): + if let max = memory.handle.limit.max, max < memoryType.min { + throw ImportError.incompatibleImportType } - } - } catch Trap.outOfBoundsMemoryAccess { - throw InstantiationError.outOfBoundsMemoryAccess - } catch { - throw error - } + result.define(i, external) + + case let (.global(globalType), .global(global)) + where globalType == global.handle.globalType: + result.define(i, external) - // Step 17. - if let startIndex = module.start { - let startFunction = try instance.functions[validating: Int(startIndex)] - _ = try startFunction.invoke([], runtime: self) + default: + throw ImportError.incompatibleImportType + } } - return instance + return result } -} -extension Runtime { @available(*, unavailable, message: "Runtime doesn't manage execution state anymore. Use Execution.step instead") public func step() throws { fatalError() @@ -205,7 +167,7 @@ extension Runtime { guard case let .function(function)? = instance.export(function) else { throw Trap.exportedFunctionNotFound(instance, name: function) } - return try function.invoke(arguments, runtime: self) + return try function.invoke(arguments) } @available(*, unavailable, message: "Use `Function.invoke` instead") @@ -214,81 +176,66 @@ extension Runtime { } } -protocol ConstEvaluationContextProtocol { - func functionRef(_ index: FunctionIndex) throws -> Reference - func globalValue(_ index: GlobalIndex) throws -> Value +/// A host-defined function which can be imported by a WebAssembly module instance. +/// +/// ## Examples +/// +/// This example section shows how to interact with WebAssembly process with ``HostFunction``. +/// +/// ### Print Int32 given by WebAssembly process +/// +/// ```swift +/// HostFunction(type: FunctionType(parameters: [.i32])) { _, args in +/// print(args[0]) +/// return [] +/// } +/// ``` +/// +/// ### Print a UTF-8 string passed by a WebAssembly module instance +/// +/// ```swift +/// HostFunction(type: FunctionType(parameters: [.i32, .i32])) { caller, args in +/// let (stringPtr, stringLength) = (Int(args[0].i32), Int(args[1].i32)) +/// guard case let .memory(memoryAddr) = caller.instance.exports["memory"] else { +/// fatalError("Missing \"memory\" export") +/// } +/// let bytesRange = stringPtr..<(stringPtr + stringLength) +/// let bytes = caller.store.memory(at: memoryAddr).data[bytesRange] +/// print(String(decoding: bytes, as: UTF8.self)) +/// return [] +/// } +/// ``` +@available(*, deprecated, renamed: "Function", message: "`HostFunction` is now unified with `Function`") +public struct HostFunction { + @available(*, deprecated, renamed: "Function.init(store:type:implementation:)", message: "Use `Engine`-based API instead") + public init(type: FunctionType, implementation: @escaping (Caller, [Value]) throws -> [Value]) { + self.type = type + self.implementation = implementation + } + + public let type: FunctionType + public let implementation: (Caller, [Value]) throws -> [Value] } -extension InternalInstance: ConstEvaluationContextProtocol { - func functionRef(_ index: FunctionIndex) throws -> Reference { - return try .function(from: self.functions[validating: Int(index)]) - } - func globalValue(_ index: GlobalIndex) throws -> Value { - return try self.globals[validating: Int(index)].value +/// A collection of globals and functions that are exported from a host module. +@available(*, deprecated, message: "Use `Imports`") +public struct HostModule { + public init( + globals: [String: Global] = [:], + memories: [String: Memory] = [:], + functions: [String: HostFunction] = [:] + ) { + self.globals = globals + self.memories = memories + self.functions = functions } -} -struct ConstEvaluationContext: ConstEvaluationContextProtocol { - let functions: ImmutableArray - var globals: [Value] - func functionRef(_ index: FunctionIndex) throws -> Reference { - return try .function(from: self.functions[validating: Int(index)]) - } - func globalValue(_ index: GlobalIndex) throws -> Value { - guard index < globals.count else { - throw GlobalEntity.createOutOfBoundsError(index: Int(index), count: globals.count) - } - return self.globals[Int(index)] - } -} + /// Names of globals exported by this module mapped to corresponding global instances. + public var globals: [String: Global] -extension ConstExpression { - func evaluate(context: C) throws -> Value { - guard self.last == .end, self.count == 2 else { - throw InstantiationError.unsupported("Expect `end` at the end of offset expression") - } - let constInst = self[0] - switch constInst { - case .i32Const(let value): return .i32(UInt32(bitPattern: value)) - case .i64Const(let value): return .i64(UInt64(bitPattern: value)) - case .f32Const(let value): return .f32(value.bitPattern) - case .f64Const(let value): return .f64(value.bitPattern) - case .globalGet(let globalIndex): - return try context.globalValue(globalIndex) - case .refNull(let type): - switch type { - case .externRef: return .ref(.extern(nil)) - case .funcRef: return .ref(.function(nil)) - } - case .refFunc(let functionIndex): - return try .ref(context.functionRef(functionIndex)) - default: - throw InstantiationError.unsupported("illegal const expression instruction: \(constInst)") - } - } -} + /// Names of memories exported by this module mapped to corresponding addresses of memory instances. + public var memories: [String: Memory] -extension WasmParser.ElementSegment { - func evaluateInits(context: C) throws -> [Reference] { - try self.initializer.map { expression -> Reference in - switch expression[0] { - case let .refFunc(index): - return try context.functionRef(index) - case .refNull(.funcRef): - return .function(nil) - case .refNull(.externRef): - return .extern(nil) - case .globalGet(let index): - let value = try context.globalValue(index) - switch value { - case .ref(.function(let addr)): - return .function(addr) - default: - throw Trap._raw("Unexpected global value type for element initializer expression") - } - default: - throw Trap._raw("Unexpected element initializer expression: \(expression)") - } - } - } + /// Names of functions exported by this module mapped to corresponding host functions. + public var functions: [String: HostFunction] } diff --git a/Sources/WasmKit/Execution/RuntimeInterceptor.swift b/Sources/WasmKit/Execution/RuntimeInterceptor.swift deleted file mode 100644 index 32f5b2d0..00000000 --- a/Sources/WasmKit/Execution/RuntimeInterceptor.swift +++ /dev/null @@ -1,29 +0,0 @@ -@_documentation(visibility: internal) -public protocol RuntimeInterceptor { - func onEnterFunction(_ function: Function, store: Store) - func onExitFunction(_ function: Function, store: Store) -} - -/// An interceptor that multiplexes multiple interceptors -@_documentation(visibility: internal) -public class MultiplexingInterceptor: RuntimeInterceptor { - private let interceptors: [RuntimeInterceptor] - - /// Creates a new multiplexing interceptor - /// - Parameter interceptors: The interceptors to multiplex - public init(_ interceptors: [RuntimeInterceptor]) { - self.interceptors = interceptors - } - - public func onEnterFunction(_ function: Function, store: Store) { - for interceptor in interceptors { - interceptor.onEnterFunction(function, store: store) - } - } - - public func onExitFunction(_ function: Function, store: Store) { - for interceptor in interceptors { - interceptor.onExitFunction(function, store: store) - } - } -} \ No newline at end of file diff --git a/Sources/WasmKit/Execution/SignpostTracer.swift b/Sources/WasmKit/Execution/SignpostTracer.swift index 841f778c..8d46a345 100644 --- a/Sources/WasmKit/Execution/SignpostTracer.swift +++ b/Sources/WasmKit/Execution/SignpostTracer.swift @@ -5,7 +5,7 @@ import os.signpost /// - Note: This interceptor is available only on Apple platforms @_documentation(visibility: internal) @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) -public class SignpostTracer: RuntimeInterceptor { +public class SignpostTracer: EngineInterceptor { /// The `OSSignposter` to use for emitting signposts let signposter: OSSignposter /// The stack of signpost states for each function call in progress @@ -22,13 +22,13 @@ public class SignpostTracer: RuntimeInterceptor { "Function Call" } - public func onEnterFunction(_ function: Function, store: Store) { - let name = store.nameRegistry.symbolicate(function.handle) + public func onEnterFunction(_ function: Function) { + let name = function.store.nameRegistry.symbolicate(function.handle) let state = self.signposter.beginInterval(functionCallName, "\(name)") signpostStates.append(state) } - public func onExitFunction(_ function: Function, store: Store) { + public func onExitFunction(_ function: Function) { let state = signpostStates.popLast()! self.signposter.endInterval(functionCallName, state) } diff --git a/Sources/WasmKit/Execution/Store.swift b/Sources/WasmKit/Execution/Store.swift index 3aa2dc3d..303ad28c 100644 --- a/Sources/WasmKit/Execution/Store.swift +++ b/Sources/WasmKit/Execution/Store.swift @@ -1,33 +1,9 @@ import WasmParser -/// A collection of globals and functions that are exported from a host module. -public struct HostModule { - public init( - globals: [String: Global] = [:], - memories: [String: Memory] = [:], - functions: [String: HostFunction] = [:] - ) { - self.globals = globals - self.memories = memories - self.functions = functions - } - - /// Names of globals exported by this module mapped to corresponding global instances. - public var globals: [String: Global] - - /// Names of memories exported by this module mapped to corresponding addresses of memory instances. - public var memories: [String: Memory] - - /// Names of functions exported by this module mapped to corresponding host functions. - public var functions: [String: HostFunction] -} - /// A container to manage WebAssembly object space. /// > Note: /// public final class Store { - var hostFunctions: [HostFunction] = [] - private var hostGlobals: [Global] = [] var nameRegistry = NameRegistry() @_spi(Fuzzing) // Consider making this public public var resourceLimiter: ResourceLimiter = DefaultResourceLimiter() @@ -37,14 +13,22 @@ public final class Store { fatalError() } - /// This property is separate from `registeredModuleInstances`, as host exports - /// won't have a corresponding module instance. - fileprivate var availableExports: [String: [String: ExternalValue]] = [:] - + /// The allocator allocating and retaining resources for this store. let allocator: StoreAllocator + /// The engine associated with this store. + public let engine: Engine + + /// Create a new store associated with the given engine. + public init(engine: Engine) { + self.engine = engine + self.allocator = StoreAllocator(funcTypeInterner: engine.funcTypeInterner) + } +} - init(funcTypeInterner: Interner) { - self.allocator = StoreAllocator(funcTypeInterner: funcTypeInterner) +extension Store: Equatable { + public static func == (lhs: Store, rhs: Store) -> Bool { + /// Use reference identity for equality comparison. + return lhs === rhs } } @@ -55,58 +39,23 @@ public struct Caller { /// - Note: This property is `nil` if a `Function` backed by a host function is called directly. public var instance: Instance? { guard let instanceHandle else { return nil } - return Instance(handle: instanceHandle, allocator: runtime.store.allocator) + return Instance(handle: instanceHandle, store: store) } - /// The runtime that called the host function. - public let runtime: Runtime + + /// The engine associated with the caller execution context. + public var engine: Engine { store.engine } + /// The store associated with the caller execution context. - public var store: Store { - runtime.store - } + public let store: Store - init(instanceHandle: InternalInstance?, runtime: Runtime) { - self.instanceHandle = instanceHandle - self.runtime = runtime - } -} + /// The runtime that called the host function. + @available(*, unavailable, message: "Use `engine` instead") + public var runtime: Runtime { fatalError() } -/// A host-defined function which can be imported by a WebAssembly module instance. -/// -/// ## Examples -/// -/// This example section shows how to interact with WebAssembly process with ``HostFunction``. -/// -/// ### Print Int32 given by WebAssembly process -/// -/// ```swift -/// HostFunction(type: FunctionType(parameters: [.i32])) { _, args in -/// print(args[0]) -/// return [] -/// } -/// ``` -/// -/// ### Print a UTF-8 string passed by a WebAssembly module instance -/// -/// ```swift -/// HostFunction(type: FunctionType(parameters: [.i32, .i32])) { caller, args in -/// let (stringPtr, stringLength) = (Int(args[0].i32), Int(args[1].i32)) -/// guard case let .memory(memoryAddr) = caller.instance.exports["memory"] else { -/// fatalError("Missing \"memory\" export") -/// } -/// let bytesRange = stringPtr..<(stringPtr + stringLength) -/// let bytes = caller.store.memory(at: memoryAddr).data[bytesRange] -/// print(String(decoding: bytes, as: UTF8.self)) -/// return [] -/// } -/// ``` -public struct HostFunction { - public init(type: FunctionType, implementation: @escaping (Caller, [Value]) throws -> [Value]) { - self.type = type - self.implementation = implementation + init(instanceHandle: InternalInstance?, store: Store) { + self.instanceHandle = instanceHandle + self.store = store } - - public let type: FunctionType - public let implementation: (Caller, [Value]) throws -> [Value] } struct HostFunctionEntity { @@ -115,52 +64,16 @@ struct HostFunctionEntity { } extension Store { - public func register(_ instance: Instance, as name: String) throws { - guard availableExports[name] == nil else { - throw ImportError.moduleInstanceAlreadyRegistered(name) - } - - availableExports[name] = instance.exports - } + @available(*, unavailable, message: "Use ``Imports/define(_:as:)`` instead. Or use ``Runtime/register(_:as:)`` as a temporary drop-in replacement.") + public func register(_ instance: Instance, as name: String) throws {} /// Register the given host module in this store with the given name. /// /// - Parameters: /// - hostModule: A host module to register. /// - name: A name to register the given host module. - public func register(_ hostModule: HostModule, as name: String, runtime: Runtime) throws { - guard availableExports[name] == nil else { - throw ImportError.moduleInstanceAlreadyRegistered(name) - } - - registerUniqueHostModule(hostModule, as: name, runtime: runtime) - } - - /// Register the given host module assuming that the given name is not registered yet. - func registerUniqueHostModule(_ hostModule: HostModule, as name: String, runtime: Runtime) { - var moduleExports = [String: ExternalValue]() - - for (globalName, global) in hostModule.globals { - moduleExports[globalName] = .global(global) - hostGlobals.append(global) - } - - for (functionName, function) in hostModule.functions { - moduleExports[functionName] = .function( - Function( - handle: allocator.allocate(hostFunction: function, runtime: runtime), - allocator: allocator - ) - ) - hostFunctions.append(function) - } - - for (memoryName, memoryAddr) in hostModule.memories { - moduleExports[memoryName] = .memory(memoryAddr) - } - - availableExports[name] = moduleExports - } + @available(*, unavailable, message: "Use ``Imports/define(_:as:)`` instead. Or use ``Runtime/register(_:as:)`` as a temporary drop-in replacement.") + public func register(_ hostModule: HostModule, as name: String, runtime: Any) throws {} @available(*, deprecated, message: "Address-based APIs has been removed; use Memory instead") public func memory(at address: Memory) -> Memory { @@ -171,44 +84,4 @@ extension Store { public func withMemory(at address: Memory, _ body: (Memory) throws -> T) rethrows -> T { try body(address) } - - func getExternalValues(_ module: Module, runtime: Runtime) throws -> [ExternalValue] { - var result = [ExternalValue]() - - for i in module.imports { - guard let moduleExports = availableExports[i.module], let external = moduleExports[i.name] else { - throw ImportError.unknownImport(moduleName: i.module, externalName: i.name) - } - - switch (i.descriptor, external) { - case let (.function(typeIndex), .function(externalFunc)): - let type = externalFunc.handle.type - guard runtime.internType(module.types[Int(typeIndex)]) == type else { - throw ImportError.incompatibleImportType - } - result.append(external) - - case let (.table(tableType), .table(table)): - if let max = table.handle.limits.max, max < tableType.limits.min { - throw ImportError.incompatibleImportType - } - result.append(external) - - case let (.memory(memoryType), .memory(memory)): - if let max = memory.handle.limit.max, max < memoryType.min { - throw ImportError.incompatibleImportType - } - result.append(external) - - case let (.global(globalType), .global(global)) - where globalType == global.handle.globalType: - result.append(external) - - default: - throw ImportError.incompatibleImportType - } - } - - return result - } } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index 9ec96b13..f2e57a34 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -106,6 +106,11 @@ struct ImmutableArray { buffer = UnsafeBufferPointer(mutable) } + /// Initializes an empty immutable array. + init() { + buffer = UnsafeBufferPointer(start: nil, count: 0) + } + /// Accesses the element at the specified position. subscript(index: Int) -> T { buffer[index] @@ -247,16 +252,13 @@ extension StoreAllocator { /// func allocate( module: Module, - runtime: Runtime, - externalValues: [ExternalValue] + engine: Engine, + resourceLimiter: any ResourceLimiter, + imports: Imports ) throws -> InternalInstance { - let resourceLimiter = runtime.store.resourceLimiter // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. let types = module.types - // Uninitialized instance - let instancePointer = instances.allocate() - let instanceHandle = InternalInstance(unsafe: instancePointer) var importedFunctions: [InternalFunction] = [] var importedTables: [InternalTable] = [] var importedMemories: [InternalMemory] = [] @@ -264,20 +266,42 @@ extension StoreAllocator { // External values imported in this module should be included in corresponding index spaces before definitions // local to to the module are added. - for external in externalValues { - switch external { - case let .function(function): - // Step 14. - importedFunctions.append(function.handle) - case let .table(table): - // Step 15. - importedTables.append(table.handle) - case let .memory(memory): - // Step 16. - importedMemories.append(memory.handle) - case let .global(global): - // Step 17. - importedGlobals.append(global.handle) + for importEntry in module.imports { + guard let (external, allocator) = imports.lookup(module: importEntry.module, name: importEntry.name) else { + throw ImportError.unknownImport(moduleName: importEntry.module, externalName: importEntry.name) + } + guard allocator === self else { + throw ImportError.importedEntityFromDifferentStore + } + + switch (importEntry.descriptor, external) { + case let (.function(typeIndex), .function(externalFunc)): + let type = externalFunc.type + guard engine.internType(module.types[Int(typeIndex)]) == type else { + throw ImportError.incompatibleImportType + } + importedFunctions.append(externalFunc) + + case let (.table(tableType), .table(table)): + if let max = table.limits.max, max < tableType.limits.min { + throw ImportError.incompatibleImportType + } + importedTables.append(table) + + case let (.memory(memoryType), .memory(memory)): + if let max = memory.limit.max, max < memoryType.min { + throw ImportError.incompatibleImportType + } + importedMemories.append(memory) + + case let (.global(globalType), .global(global)): + guard globalType == global.globalType else { + throw ImportError.incompatibleImportType + } + importedGlobals.append(global) + + default: + throw ImportError.incompatibleImportType } } @@ -296,12 +320,26 @@ extension StoreAllocator { } } + // Uninitialized instance + let instancePointer = instances.allocate() + var instanceInitialized = false + defer { + // If the instance is not initialized due to an exception, initialize it with an empty instance + // to allow bump deallocation by the bump allocator. + // This is not optimal as it leaves an empty instance without deallocating the space but + // good at code simplicity. + if !instanceInitialized { + instancePointer.initialize(to: .empty) + } + } + let instanceHandle = InternalInstance(unsafe: instancePointer) + // Step 2. let functions = allocateEntities( imports: importedFunctions, internals: module.functions, allocateHandle: { f, index in - allocate(function: f, index: FunctionIndex(index), instance: instanceHandle, runtime: runtime) + allocate(function: f, index: FunctionIndex(index), instance: instanceHandle, engine: engine) } ) @@ -408,22 +446,22 @@ extension StoreAllocator { hasDataCount: module.hasDataCount ) instancePointer.initialize(to: instanceEntity) + instanceInitialized = true return instanceHandle } /// > Note: /// - /// TODO: Mark as private - func allocate( + private func allocate( function: GuestFunction, index: FunctionIndex, instance: InternalInstance, - runtime: Runtime + engine: Engine ) -> InternalFunction { let code = InternalUncompiledCode(unsafe: codes.allocate(initializing: function.code)) let pointer = functions.allocate( initializing: WasmFunctionEntity( - index: index, type: runtime.internType(function.type), + index: index, type: engine.internType(function.type), code: code, instance: instance ) @@ -431,11 +469,14 @@ extension StoreAllocator { return InternalFunction.wasm(EntityHandle(unsafe: pointer)) } - func allocate(hostFunction: HostFunction, runtime: Runtime) -> InternalFunction { + internal func allocate( + type: FunctionType, + implementation: @escaping (Caller, [Value]) throws -> [Value], + engine: Engine + ) -> InternalFunction { let pointer = hostFunctions.allocate( initializing: HostFunctionEntity( - type: runtime.internType(hostFunction.type), - implementation: hostFunction.implementation + type: engine.internType(type), implementation: implementation ) ) return InternalFunction.host(EntityHandle(unsafe: pointer)) @@ -443,7 +484,7 @@ extension StoreAllocator { /// > Note: /// - private func allocate(tableType: TableType, resourceLimiter: any ResourceLimiter) throws -> InternalTable { + func allocate(tableType: TableType, resourceLimiter: any ResourceLimiter) throws -> InternalTable { let pointer = try tables.allocate(initializing: TableEntity(tableType, resourceLimiter: resourceLimiter)) return InternalTable(unsafe: pointer) } diff --git a/Sources/WasmKit/Imports.swift b/Sources/WasmKit/Imports.swift new file mode 100644 index 00000000..8e8afdaf --- /dev/null +++ b/Sources/WasmKit/Imports.swift @@ -0,0 +1,75 @@ +import struct WasmParser.Import + +/// A set of entities used to import values when instantiating a module. +public struct Imports { + private var definitions: [String: [String: ExternalValue]] = [:] + + /// Initializes a new instance of `Imports`. + public init() { + } + + /// Define a value to be imported by the given module and name. + public mutating func define(module: String, name: String, _ value: Extern) { + definitions[module, default: [:]][name] = value.externalValue + } + + /// Define a set of values to be imported by the given module. + /// - Parameters: + /// - module: The module name to be used for resolving the imports. + /// - values: The values to be imported keyed by their name. + public mutating func define(module: String, _ values: Exports) { + definitions[module, default: [:]].merge(values.map { ($0, $1) }, uniquingKeysWith: { _, new in new }) + } + + mutating func define(_ importEntry: Import, _ value: ExternalValue) { + define(module: importEntry.module, name: importEntry.name, value) + } + + /// Lookup a value to be imported by the given module and name. + func lookup(module: String, name: String) -> (InternalExternalValue, StoreAllocator)? { + definitions[module]?[name]?.internalize() + } +} + +/// A value that can be imported or exported from an instance. +public protocol ExternalValueConvertible { + var externalValue: ExternalValue { get } +} + +extension ExternalValue: ExternalValueConvertible { + public var externalValue: ExternalValue { self } +} + +extension Memory: ExternalValueConvertible { + public var externalValue: ExternalValue { .memory(self) } +} + +extension Table: ExternalValueConvertible { + public var externalValue: ExternalValue { .table(self) } +} + +extension Global: ExternalValueConvertible { + public var externalValue: ExternalValue { .global(self) } +} + +extension Function: ExternalValueConvertible { + public var externalValue: ExternalValue { .function(self) } +} + +extension Imports: ExpressibleByDictionaryLiteral { + public typealias Key = String + public struct Value: ExpressibleByDictionaryLiteral { + public typealias Key = String + public typealias Value = ExternalValueConvertible + + let definitions: [String: ExternalValue] + + public init(dictionaryLiteral elements: (String, any Value)...) { + self.definitions = Dictionary(uniqueKeysWithValues: elements.map { ($0.0, $0.1.externalValue) }) + } + } + + public init(dictionaryLiteral elements: (String, Value)...) { + self.definitions = Dictionary(uniqueKeysWithValues: elements.map { ($0.0, $0.1.definitions) }) + } +} diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 13df0fbb..af789fd9 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -116,6 +116,92 @@ public struct Module { return typeSection[Int(index)] } + /// Instantiate this module in the given imports. + /// + /// - Parameters: + /// - store: The ``Store`` to allocate the instance in. + /// - imports: The imports to use for instantiation. All imported entities + /// must be allocated in the given store. + public func instantiate(store: Store, imports: Imports = [:]) throws -> Instance { + Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) + } + + /// > Note: + /// + private func instantiateHandle(store: Store, imports: Imports) throws -> InternalInstance { + // Steps 5-8. + + // Step 9. + // Process `elem.init` evaluation during allocation + + // Step 11. + let instance = try store.allocator.allocate( + module: self, engine: store.engine, + resourceLimiter: store.resourceLimiter, + imports: imports + ) + + if let nameSection = customSections.first(where: { $0.name == "name" }) { + // FIXME?: Just ignore parsing error of name section for now. + // Should emit warning instead of just discarding it? + try? store.nameRegistry.register(instance: instance, nameSection: nameSection) + } + + // Step 12-13. + + // Steps 14-15. + do { + for element in elements { + guard case let .active(tableIndex, offset) = element.mode else { continue } + let offsetValue = try offset.evaluate(context: instance) + let table = try instance.tables[validating: Int(tableIndex)] + try table.withValue { table in + guard let offset = offsetValue.maybeAddressOffset(table.limits.isMemory64) else { + throw InstantiationError.unsupported( + "Expect \(ValueType.addressType(isMemory64: table.limits.isMemory64)) offset of active element segment but got \(offsetValue)" + ) + } + let references = try element.evaluateInits(context: instance) + try table.initialize( + elements: references, from: 0, to: Int(offset), count: references.count + ) + } + } + } catch Trap.undefinedElement, Trap.tableSizeOverflow, Trap.outOfBoundsTableAccess { + throw InstantiationError.outOfBoundsTableAccess + } catch { + throw error + } + + // Step 16. + do { + for case let .active(data) in data { + let offsetValue = try data.offset.evaluate(context: instance) + let memory = try instance.memories[validating: Int(data.index)] + try memory.withValue { memory in + guard let offset = offsetValue.maybeAddressOffset(memory.limit.isMemory64) else { + throw InstantiationError.unsupported( + "Expect \(ValueType.addressType(isMemory64: memory.limit.isMemory64)) offset of active data segment but got \(offsetValue)" + ) + } + try memory.write(offset: Int(offset), bytes: data.initializer) + } + } + } catch Trap.outOfBoundsMemoryAccess { + throw InstantiationError.outOfBoundsMemoryAccess + } catch { + throw error + } + + // Step 17. + if let startIndex = start { + let startFunction = try instance.functions[validating: Int(startIndex)] + _ = try startFunction.invoke([], store: store) + } + + return instance + } + /// Materialize lazily-computed elements in this module public mutating func materializeAll() throws { let allocator = ISeqAllocator() diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index dc81c7b7..0d8e0f61 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -559,10 +559,10 @@ struct InstructionTranslator: InstructionVisitor { fileprivate var insertingPC: MetaProgramCounter { MetaProgramCounter(offsetFromHead: instructions.count) } - let runtimeConfiguration: RuntimeConfiguration + let engineConfiguration: EngineConfiguration - init(runtimeConfiguration: RuntimeConfiguration) { - self.runtimeConfiguration = runtimeConfiguration + init(engineConfiguration: EngineConfiguration) { + self.engineConfiguration = engineConfiguration } func assertDanglingLabels() throws { @@ -585,7 +585,7 @@ struct InstructionTranslator: InstructionVisitor { private mutating func assign(at index: Int, _ instruction: Instruction) { trace("assign: \(instruction)") - let headSlot = instruction.headSlot(threadingModel: runtimeConfiguration.threadingModel) + let headSlot = instruction.headSlot(threadingModel: engineConfiguration.threadingModel) trace(" [\(index)] = 0x\(String(headSlot, radix: 16))") self.instructions[index] = headSlot if let immediate = instruction.rawImmediate { @@ -630,7 +630,7 @@ struct InstructionTranslator: InstructionVisitor { mutating func emit(_ instruction: Instruction, resultRelink: ResultRelink? = nil) { self.lastEmission = LastEmission(position: insertingPC, resultRelink: resultRelink) trace("emitInstruction: \(instruction)") - emitSlot(instruction.headSlot(threadingModel: runtimeConfiguration.threadingModel)) + emitSlot(instruction.headSlot(threadingModel: engineConfiguration.threadingModel)) if let immediate = instruction.rawImmediate { var slots: [CodeSlot] = [] immediate.emit(to: { slots.append($0) }) @@ -807,7 +807,7 @@ struct InstructionTranslator: InstructionVisitor { init( allocator: ISeqAllocator, - runtimeConfiguration: RuntimeConfiguration, + engineConfiguration: EngineConfiguration, funcTypeInterner: Interner, module: Context, type: FunctionType, @@ -820,7 +820,7 @@ struct InstructionTranslator: InstructionVisitor { self.funcTypeInterner = funcTypeInterner self.type = type self.module = module - self.iseqBuilder = ISeqBuilder(runtimeConfiguration: runtimeConfiguration) + self.iseqBuilder = ISeqBuilder(engineConfiguration: engineConfiguration) self.controlStack = ControlStack() self.stackLayout = try StackLayout( type: type, diff --git a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift index 0c4d6377..67de2d90 100644 --- a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift +++ b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift @@ -4,17 +4,30 @@ import WasmKit public typealias WASIBridgeToHost = WASI.WASIBridgeToHost extension WASIBridgeToHost { + public func link(to imports: inout Imports, store: Store) { + for (moduleName, module) in wasiHostModules { + for (name, function) in module.functions { + imports.define( + module: moduleName, + name: name, + Function(store: store, type: function.type, body: makeHostFunction(function)) + ) + } + } + } + + @available(*, deprecated, renamed: "link(to:store:)", message: "Use `Engine`-based API instead") public var hostModules: [String: HostModule] { wasiHostModules.mapValues { (module: WASIHostModule) -> HostModule in HostModule( functions: module.functions.mapValues { function -> HostFunction in - makeHostFunction(function) + HostFunction(type: function.type, implementation: makeHostFunction(function)) }) } } - private func makeHostFunction(_ function: WASIHostFunction) -> HostFunction { - HostFunction(type: function.type) { caller, values -> [Value] in + private func makeHostFunction(_ function: WASIHostFunction) -> ((Caller, [Value]) throws -> [Value]) { + { caller, values -> [Value] in guard case let .memory(memory) = caller.instance?.export("memory") else { throw WASIError(description: "Missing required \"memory\" export") } @@ -22,12 +35,20 @@ extension WASIBridgeToHost { } } - public func start(_ instance: Instance, runtime: Runtime) throws -> UInt32 { + public func start(_ instance: Instance) throws -> UInt32 { do { - _ = try runtime.invoke(instance, function: "_start") + guard let start = instance.exports[function: "_start"] else { + throw WASIError(description: "Missing required \"_start\" function") + } + _ = try start() } catch let code as WASIExitCode { return code.code } return 0 } + + @available(*, deprecated, message: "Use `Engine`-based API instead") + public func start(_ instance: Instance, runtime: Runtime) throws -> UInt32 { + return try start(instance) + } } diff --git a/Sources/WasmParser/Docs.docc/Docs.md b/Sources/WasmParser/Docs.docc/Docs.md index 1f8e2f44..e38c4c7f 100644 --- a/Sources/WasmParser/Docs.docc/Docs.md +++ b/Sources/WasmParser/Docs.docc/Docs.md @@ -60,7 +60,6 @@ while let payload = try parser.parseNext() { ### Core Module Elements -- ``FunctionType`` - ``Import`` - ``ImportDescriptor`` - ``Export`` diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 90821e53..eaaebc36 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -1243,9 +1243,7 @@ public struct InstructionTracingVisitor: InstructionVisit /// A visitor for WebAssembly instructions. /// /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. -/// See the following parsing functions: -/// - ``parseExpression(bytes:features:hasDataCount:visitor:)`` -/// - ``parseExpression(stream:features:hasDataCount:visitor:)`` +/// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { /// The return type of visitor methods. diff --git a/Tests/WASITests/IntegrationTests.swift b/Tests/WASITests/IntegrationTests.swift index 8f363d53..ecb97913 100644 --- a/Tests/WASITests/IntegrationTests.swift +++ b/Tests/WASITests/IntegrationTests.swift @@ -116,10 +116,13 @@ final class IntegrationTests: XCTestCase { $0[$1] = suitePath.appendingPathComponent($1).path } ) - let runtime = Runtime(hostModules: wasi.hostModules) + let engine = Engine() + let store = Store(engine: engine) + var imports = Imports() + wasi.link(to: &imports, store: store) let module = try parseWasm(filePath: FilePath(path.path)) - let instance = try runtime.instantiate(module: module) - let exitCode = try wasi.start(instance, runtime: runtime) + let instance = try module.instantiate(store: store, imports: imports) + let exitCode = try wasi.start(instance) XCTAssertEqual(exitCode, manifest.exitCode ?? 0, path.path) } } diff --git a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift index b4990e95..6e8e0334 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift @@ -6,9 +6,9 @@ import XCTest class RuntimeSmokeTests: XCTestCase { func testCallExportByGuest() throws { var harness = try RuntimeTestHarness(fixture: "Smoke") - try harness.build(link: SmokeTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: SmokeTestWorld.link) { (instance) in let component = SmokeTestWorld(instance: instance) - _ = try component.hello(runtime: runtime) + _ = try component.hello() } } } diff --git a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTestHarness.swift b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTestHarness.swift index 04886fae..957d0060 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTestHarness.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTestHarness.swift @@ -123,21 +123,24 @@ struct RuntimeTestHarness { /// Build up WebAssembly module from the fixture and instantiate WasmKit runtime with the module. mutating func build( - link: (inout [String: HostModule]) -> Void, - run: (Runtime, Instance) throws -> Void + link: (inout Imports, Store) -> Void, + run: (Instance) throws -> Void ) throws { for compile in [compileForEmbedded, compileForWASI] { defer { cleanupTemporaryFiles() } let compiled = try compile(collectGuestInputFiles()) + let engine = Engine() + let store = Store(engine: engine) + let wasi = try WASIBridgeToHost(args: [compiled.path]) - var hostModules: [String: HostModule] = wasi.hostModules - link(&hostModules) + var imports = Imports() + wasi.link(to: &imports, store: store) + link(&imports, store) let module = try parseWasm(filePath: .init(compiled.path)) - let runtime = Runtime(hostModules: hostModules) - let instance = try runtime.instantiate(module: module) - try run(runtime, instance) + let instance = try module.instantiate(store: store, imports: imports) + try run(instance) } } diff --git a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift index cee5de21..5f6fe490 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift @@ -7,66 +7,66 @@ import XCTest class RuntimeTypesTests: XCTestCase { func testNumber() throws { var harness = try RuntimeTestHarness(fixture: "Number") - try harness.build(link: NumberTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: NumberTestWorld.link) { (instance) in let component = NumberTestWorld(instance: instance) - XCTAssertEqual(try component.roundtripBool(runtime: runtime, v: true), true) - XCTAssertEqual(try component.roundtripBool(runtime: runtime, v: false), false) + XCTAssertEqual(try component.roundtripBool(v: true), true) + XCTAssertEqual(try component.roundtripBool(v: false), false) for value in [0, 1, -1, .max, .min] as [Int8] { - XCTAssertEqual(try component.roundtripS8(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripS8(v: value), value) } for value in [0, 1, -1, .max, .min] as [Int16] { - XCTAssertEqual(try component.roundtripS16(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripS16(v: value), value) } for value in [0, 1, -1, .max, .min] as [Int32] { - XCTAssertEqual(try component.roundtripS32(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripS32(v: value), value) } for value in [0, 1, -1, .max, .min] as [Int64] { - XCTAssertEqual(try component.roundtripS64(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripS64(v: value), value) } for value in [0, 1, .max] as [UInt8] { - XCTAssertEqual(try component.roundtripU8(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripU8(v: value), value) } for value in [0, 1, .max] as [UInt16] { - XCTAssertEqual(try component.roundtripU16(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripU16(v: value), value) } for value in [0, 1, .max] as [UInt32] { - XCTAssertEqual(try component.roundtripU32(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripU32(v: value), value) } for value in [0, 1, .max] as [UInt64] { - XCTAssertEqual(try component.roundtripU64(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtripU64(v: value), value) } - let value1 = try component.retptrU8(runtime: runtime) + let value1 = try component.retptrU8() XCTAssertEqual(value1.0, 1) XCTAssertEqual(value1.1, 2) - let value2 = try component.retptrU16(runtime: runtime) + let value2 = try component.retptrU16() XCTAssertEqual(value2.0, 1) XCTAssertEqual(value2.1, 2) - let value3 = try component.retptrU32(runtime: runtime) + let value3 = try component.retptrU32() XCTAssertEqual(value3.0, 1) XCTAssertEqual(value3.1, 2) - let value4 = try component.retptrU64(runtime: runtime) + let value4 = try component.retptrU64() XCTAssertEqual(value4.0, 1) XCTAssertEqual(value4.1, 2) - let value5 = try component.retptrS8(runtime: runtime) + let value5 = try component.retptrS8() XCTAssertEqual(value5.0, 1) XCTAssertEqual(value5.1, -2) - let value6 = try component.retptrS16(runtime: runtime) + let value6 = try component.retptrS16() XCTAssertEqual(value6.0, 1) XCTAssertEqual(value6.1, -2) - let value7 = try component.retptrS32(runtime: runtime) + let value7 = try component.retptrS32() XCTAssertEqual(value7.0, 1) XCTAssertEqual(value7.1, -2) - let value8 = try component.retptrS64(runtime: runtime) + let value8 = try component.retptrS64() XCTAssertEqual(value8.0, 1) XCTAssertEqual(value8.1, -2) } @@ -74,36 +74,36 @@ class RuntimeTypesTests: XCTestCase { func testChar() throws { var harness = try RuntimeTestHarness(fixture: "Char") - try harness.build(link: CharTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: CharTestWorld.link) { (instance) in let component = CharTestWorld(instance: instance) for char in "abcd🍏👨‍👩‍👦‍👦".unicodeScalars { - XCTAssertEqual(try component.roundtrip(runtime: runtime, v: char), char) + XCTAssertEqual(try component.roundtrip(v: char), char) } } } func testOption() throws { var harness = try RuntimeTestHarness(fixture: "Option") - try harness.build(link: OptionTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: OptionTestWorld.link) { (instance) in let component = OptionTestWorld(instance: instance) - let value1 = try component.returnNone(runtime: runtime) + let value1 = try component.returnNone() XCTAssertEqual(value1, nil) - let value2 = try component.returnOptionF32(runtime: runtime) + let value2 = try component.returnOptionF32() XCTAssertEqual(value2, .some(0.5)) - let value3 = try component.returnOptionTypedef(runtime: runtime) + let value3 = try component.returnOptionTypedef() XCTAssertEqual(value3, .some(42)) - let value4 = try component.returnSomeNone(runtime: runtime) + let value4 = try component.returnSomeNone() XCTAssertEqual(value4, .some(nil)) - let value5 = try component.returnSomeSome(runtime: runtime) + let value5 = try component.returnSomeSome() XCTAssertEqual(value5, .some(33_550_336)) for value in [.some(1), nil] as [UInt32?] { - let value6 = try component.roundtrip(runtime: runtime, v: value) + let value6 = try component.roundtrip(v: value) XCTAssertEqual(value6, value) } } @@ -111,17 +111,17 @@ class RuntimeTypesTests: XCTestCase { func testRecord() throws { var harness = try RuntimeTestHarness(fixture: "Record") - try harness.build(link: RecordTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: RecordTestWorld.link) { (instance) in let component = RecordTestWorld(instance: instance) - _ = try component.returnEmpty(runtime: runtime) + _ = try component.returnEmpty() - _ = try component.roundtripEmpty(runtime: runtime, v: RecordTestWorld.RecordEmpty()) + _ = try component.roundtripEmpty(v: RecordTestWorld.RecordEmpty()) - let value3 = try component.returnPadded(runtime: runtime) + let value3 = try component.returnPadded() XCTAssertEqual(value3.f1, 28) XCTAssertEqual(value3.f2, 496) - let value4 = try component.roundtripPadded(runtime: runtime, v: RecordTestWorld.RecordPadded(f1: 6, f2: 8128)) + let value4 = try component.roundtripPadded(v: RecordTestWorld.RecordPadded(f1: 6, f2: 8128)) XCTAssertEqual(value4.f1, 6) XCTAssertEqual(value4.f2, 8128) } @@ -129,52 +129,52 @@ class RuntimeTypesTests: XCTestCase { func testString() throws { var harness = try RuntimeTestHarness(fixture: "String") - try harness.build(link: StringTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: StringTestWorld.link) { (instance) in let component = StringTestWorld(instance: instance) - XCTAssertEqual(try component.returnEmpty(runtime: runtime), "") - XCTAssertEqual(try component.roundtrip(runtime: runtime, v: "ok"), "ok") - XCTAssertEqual(try component.roundtrip(runtime: runtime, v: "🍏"), "🍏") - XCTAssertEqual(try component.roundtrip(runtime: runtime, v: "\u{0}"), "\u{0}") + XCTAssertEqual(try component.returnEmpty(), "") + XCTAssertEqual(try component.roundtrip(v: "ok"), "ok") + XCTAssertEqual(try component.roundtrip(v: "🍏"), "🍏") + XCTAssertEqual(try component.roundtrip(v: "\u{0}"), "\u{0}") let longString = String(repeating: "a", count: 1000) - XCTAssertEqual(try component.roundtrip(runtime: runtime, v: longString), longString) + XCTAssertEqual(try component.roundtrip(v: longString), longString) } } func testList() throws { var harness = try RuntimeTestHarness(fixture: "List") - try harness.build(link: ListTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: ListTestWorld.link) { (instance) in let component = ListTestWorld(instance: instance) - XCTAssertEqual(try component.returnEmpty(runtime: runtime), []) + XCTAssertEqual(try component.returnEmpty(), []) for value in [[], [1, 2, 3]] as [[UInt8]] { - XCTAssertEqual(try component.roundtrip(runtime: runtime, v: value), value) + XCTAssertEqual(try component.roundtrip(v: value), value) } let value1 = ["foo", "bar"] - XCTAssertEqual(try component.roundtripNonPod(runtime: runtime, v: value1), value1) + XCTAssertEqual(try component.roundtripNonPod(v: value1), value1) let value2 = [["apple", "pineapple"], ["grape", "grapefruit"], [""]] - XCTAssertEqual(try component.roundtripListList(runtime: runtime, v: value2), value2) + XCTAssertEqual(try component.roundtripListList(v: value2), value2) } } func testVariant() throws { var harness = try RuntimeTestHarness(fixture: "Variant") - try harness.build(link: VariantTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: VariantTestWorld.link) { (instance) in let component = VariantTestWorld(instance: instance) - XCTAssertEqual(try component.returnSingle(runtime: runtime), .a(33_550_336)) + XCTAssertEqual(try component.returnSingle(), .a(33_550_336)) - let value1 = try component.returnLarge(runtime: runtime) + let value1 = try component.returnLarge() guard case let .c256(value1) = value1 else { XCTFail("unexpected variant case \(value1)") return } XCTAssertEqual(value1, 42) - let value2 = try component.roundtripLarge(runtime: runtime, v: .c000) + let value2 = try component.roundtripLarge(v: .c000) guard case .c000 = value2 else { XCTFail("unexpected variant case \(value2)") return } - let value3 = try component.roundtripLarge(runtime: runtime, v: .c256(24)) + let value3 = try component.roundtripLarge(v: .c256(24)) guard case let .c256(value3) = value3 else { XCTFail("unexpected variant case \(value3)") return @@ -185,50 +185,50 @@ class RuntimeTypesTests: XCTestCase { func testResult() throws { var harness = try RuntimeTestHarness(fixture: "Result") - try harness.build(link: ResultTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: ResultTestWorld.link) { (instance) in let component = ResultTestWorld(instance: instance) - let value4 = try component.roundtripResult(runtime: runtime, v: .success(())) + let value4 = try component.roundtripResult(v: .success(())) guard case .success = value4 else { XCTFail("unexpected variant case \(value4)") return } - let value5 = try component.roundtripResult(runtime: runtime, v: .failure(.init(()))) + let value5 = try component.roundtripResult(v: .failure(.init(()))) guard case .failure = value5 else { XCTFail("unexpected variant case \(value5)") return } - let value6 = try component.roundtripResultOk(runtime: runtime, v: .success(8128)) + let value6 = try component.roundtripResultOk(v: .success(8128)) guard case .success(let value5) = value6 else { XCTFail("unexpected variant case \(value6)") return } XCTAssertEqual(value5, 8128) - let value7 = try component.roundtripResultOkError(runtime: runtime, v: .success(496)) + let value7 = try component.roundtripResultOkError(v: .success(496)) XCTAssertEqual(value7, .success(496)) - let value8 = try component.roundtripResultOkError(runtime: runtime, v: .failure(.init("bad"))) + let value8 = try component.roundtripResultOkError(v: .failure(.init("bad"))) XCTAssertEqual(value8, .failure(.init("bad"))) } } func testEnum() throws { var harness = try RuntimeTestHarness(fixture: "Enum") - try harness.build(link: EnumTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: EnumTestWorld.link) { (instance) in let component = EnumTestWorld(instance: instance) - let value1 = try component.roundtripSingle(runtime: runtime, v: .a) + let value1 = try component.roundtripSingle(v: .a) XCTAssertEqual(value1, .a) for c in [EnumTestWorld.Large.c000, .c127, .c128, .c255, .c256] { - let value2 = try component.roundtripLarge(runtime: runtime, v: c) + let value2 = try component.roundtripLarge(v: c) XCTAssertEqual(value2, c) } - let value3 = try component.returnByPointer(runtime: runtime) + let value3 = try component.returnByPointer() XCTAssertEqual(value3.0, .a) XCTAssertEqual(value3.1, .b) } @@ -236,33 +236,33 @@ class RuntimeTypesTests: XCTestCase { func testFlags() throws { var harness = try RuntimeTestHarness(fixture: "Flags") - try harness.build(link: FlagsTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: FlagsTestWorld.link) { (instance) in let component = FlagsTestWorld(instance: instance) - XCTAssertEqual(try component.roundtripSingle(runtime: runtime, v: []), []) + XCTAssertEqual(try component.roundtripSingle(v: []), []) let value1: FlagsTestWorld.Single = .a - XCTAssertEqual(try component.roundtripSingle(runtime: runtime, v: value1), value1) + XCTAssertEqual(try component.roundtripSingle(v: value1), value1) let value2: FlagsTestWorld.ManyU8 = [.f00, .f01, .f07] - XCTAssertEqual(try component.roundtripManyU8(runtime: runtime, v: value2), value2) + XCTAssertEqual(try component.roundtripManyU8(v: value2), value2) let value3: FlagsTestWorld.ManyU16 = [.f00, .f01, .f07, .f15] - XCTAssertEqual(try component.roundtripManyU16(runtime: runtime, v: value3), value3) + XCTAssertEqual(try component.roundtripManyU16(v: value3), value3) let value4: FlagsTestWorld.ManyU32 = [.f00, .f01, .f07, .f15, .f23, .f31] - XCTAssertEqual(try component.roundtripManyU32(runtime: runtime, v: value4), value4) + XCTAssertEqual(try component.roundtripManyU32(v: value4), value4) let value5: FlagsTestWorld.ManyU64 = [.f00, .f01, .f07, .f15, .f23, .f31, .f39, .f47, .f55, .f63] - XCTAssertEqual(try component.roundtripManyU64(runtime: runtime, v: value5), value5) + XCTAssertEqual(try component.roundtripManyU64(v: value5), value5) } } func testTuple() throws { var harness = try RuntimeTestHarness(fixture: "Tuple") - try harness.build(link: TupleTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: TupleTestWorld.link) { (instance) in let component = TupleTestWorld(instance: instance) - let value1 = try component.roundtrip(runtime: runtime, v: (true, 42)) + let value1 = try component.roundtrip(v: (true, 42)) XCTAssertEqual(value1.0, true) XCTAssertEqual(value1.1, 42) } @@ -270,13 +270,13 @@ class RuntimeTypesTests: XCTestCase { func testInterface() throws { var harness = try RuntimeTestHarness(fixture: "Interface") - try harness.build(link: InterfaceTestWorld.link(_:)) { (runtime, instance) in + try harness.build(link: InterfaceTestWorld.link) { (instance) in let component = InterfaceTestWorld(instance: instance) - let value1 = try component.roundtripT1(runtime: runtime, v: 42) + let value1 = try component.roundtripT1(v: 42) XCTAssertEqual(value1, 42) let iface = InterfaceTestWorld.IfaceFuncs(instance: instance) - let value2 = try iface.roundtripU8(runtime: runtime, v: 43) + let value2 = try iface.roundtripU8(v: 43) XCTAssertEqual(value2, 43) } } @@ -284,7 +284,7 @@ class RuntimeTypesTests: XCTestCase { func testNaming() throws { // Ensure compilation succeed for both host and guest var harness = try RuntimeTestHarness(fixture: "Naming") - try harness.build(link: NamingTestWorld.link(_:), run: { _, _ in }) + try harness.build(link: NamingTestWorld.link, run: { _ in }) } } diff --git a/Tests/WasmKitTests/Execution/HostModuleTests.swift b/Tests/WasmKitTests/Execution/HostModuleTests.swift index 700b28ee..ad30e170 100644 --- a/Tests/WasmKitTests/Execution/HostModuleTests.swift +++ b/Tests/WasmKitTests/Execution/HostModuleTests.swift @@ -6,23 +6,13 @@ import XCTest final class HostModuleTests: XCTestCase { func testImportMemory() throws { - let runtime = Runtime() + let engine = Engine() + let store = Store(engine: engine) let memoryType = MemoryType(min: 1, max: nil) - let memory = try runtime.store.allocator.allocate( - memoryType: memoryType, resourceLimiter: DefaultResourceLimiter()) - try runtime.store.register( - HostModule( - memories: [ - "memory": Memory( - handle: memory, - allocator: runtime.store - .allocator - ) - ] - ), - as: "env", - runtime: runtime - ) + let memory = try WasmKit.Memory(store: store, type: memoryType) + let imports: Imports = [ + "env": ["memory": memory] + ] let module = try parseWasm( bytes: wat2wasm( @@ -31,13 +21,14 @@ final class HostModuleTests: XCTestCase { (import "env" "memory" (memory 1)) ) """)) - XCTAssertNoThrow(try runtime.instantiate(module: module)) + XCTAssertNoThrow(try module.instantiate(store: store, imports: imports)) // Ensure the allocated address is valid _ = memory.data } func testReentrancy() throws { - let runtime = Runtime() + let engine = Engine() + let store = Store(engine: engine) let voidSignature = WasmTypes.FunctionType(parameters: [], results: []) let module = try parseWasm( bytes: wat2wasm( @@ -59,28 +50,28 @@ final class HostModuleTests: XCTestCase { var isExecutingFoo = false var isQuxCalled = false - let hostModule = HostModule( - functions: [ - "bar": HostFunction(type: voidSignature) { caller, _ in + let imports: Imports = [ + "env": [ + "bar": Function(store: store, type: voidSignature) { caller, _ in // Ensure "invoke" executes instructions under the current call XCTAssertFalse(isExecutingFoo, "bar should not be called recursively") isExecutingFoo = true defer { isExecutingFoo = false } let foo = try XCTUnwrap(caller.instance?.exportedFunction(name: "baz")) - _ = try foo.invoke([], runtime: caller.runtime) + _ = try foo() return [] }, - "qux": HostFunction(type: voidSignature) { _, _ in + "qux": Function(store: store, type: voidSignature) { caller, _ in XCTAssertTrue(isExecutingFoo) isQuxCalled = true return [] }, ] - ) - try runtime.store.register(hostModule, as: "env", runtime: runtime) - let instance = try runtime.instantiate(module: module) + ] + let instance = try module.instantiate(store: store, imports: imports) // Check foo(wasm) -> bar(host) -> baz(wasm) -> qux(host) - _ = try runtime.invoke(instance, function: "foo") + let foo = try XCTUnwrap(instance.exports[function: "foo"]) + try foo() XCTAssertTrue(isQuxCalled) } } diff --git a/Tests/WasmKitTests/Execution/Runtime/StoreAllocatorTests.swift b/Tests/WasmKitTests/Execution/Runtime/StoreAllocatorTests.swift index d9385588..decdee88 100644 --- a/Tests/WasmKitTests/Execution/Runtime/StoreAllocatorTests.swift +++ b/Tests/WasmKitTests/Execution/Runtime/StoreAllocatorTests.swift @@ -34,9 +34,10 @@ final class StoreAllocatorTests: XCTestCase { (memory (;0;) 0) (export "a" (memory 0))) """)) - let runtime = Runtime() - _ = try runtime.instantiate(module: module) - weakAllocator = runtime.store.allocator + let engine = Engine() + let store = Store(engine: engine) + _ = try module.instantiate(store: store) + weakAllocator = store.allocator } XCTAssertNil(weakAllocator) } diff --git a/Tests/WasmKitTests/ExecutionTests.swift b/Tests/WasmKitTests/ExecutionTests.swift index 62aab31c..297d8a41 100644 --- a/Tests/WasmKitTests/ExecutionTests.swift +++ b/Tests/WasmKitTests/ExecutionTests.swift @@ -20,9 +20,11 @@ final class ExecutionTests: XCTestCase { """ ) ) - let runtime = Runtime() - let instance = try runtime.instantiate(module: module) - let results = try runtime.invoke(instance, function: "_start") + let engine = Engine() + let store = Store(engine: engine) + let instance = try module.instantiate(store: store) + let _start = try XCTUnwrap(instance.exports[function: "_start"]) + let results = try _start() XCTAssertEqual(results, [.i32(42)]) } @@ -41,9 +43,11 @@ final class ExecutionTests: XCTestCase { """ ) ) - let runtime = Runtime() - let instance = try runtime.instantiate(module: module) - let results = try runtime.invoke(instance, function: "_start") + let engine = Engine() + let store = Store(engine: engine) + let instance = try module.instantiate(store: store) + let _start = try XCTUnwrap(instance.exports[function: "_start"]) + let results = try _start() XCTAssertEqual(results, [.i32(42)]) } } diff --git a/Tests/WasmKitTests/Spectest/Spectest.swift b/Tests/WasmKitTests/Spectest/Spectest.swift index 296f7238..f714bad8 100644 --- a/Tests/WasmKitTests/Spectest/Spectest.swift +++ b/Tests/WasmKitTests/Spectest/Spectest.swift @@ -10,7 +10,7 @@ public func spectest( exclude: String?, verbose: Bool = false, parallel: Bool = true, - configuration: RuntimeConfiguration = .init() + configuration: EngineConfiguration = .init() ) async throws -> Bool { let printVerbose = verbose @Sendable func log(_ message: String, verbose: Bool = false) { diff --git a/Tests/WasmKitTests/Spectest/TestCase.swift b/Tests/WasmKitTests/Spectest/TestCase.swift index 96420ac9..b26a94f7 100644 --- a/Tests/WasmKitTests/Spectest/TestCase.swift +++ b/Tests/WasmKitTests/Spectest/TestCase.swift @@ -100,8 +100,26 @@ enum Result { } } +struct SpectestError: Error, CustomStringConvertible { + var description: String + init(_ description: String) { + self.description = description + } +} + class WastRunContext { + let store: Store + var engine: Engine { store.engine } + let rootPath: String private var namedModuleInstances: [String: Instance] = [:] + var currentInstance: Instance? + var importsSpace = Imports() + + init(store: Store, rootPath: String) { + self.store = store + self.rootPath = rootPath + } + func lookupInstance(_ name: String) -> Instance? { return namedModuleInstances[name] } @@ -111,25 +129,23 @@ class WastRunContext { } extension TestCase { - func run(spectestModule: Module, configuration: RuntimeConfiguration, handler: @escaping (TestCase, Location, Result) -> Void) throws { - let runtime = Runtime(configuration: configuration) - let hostModuleInstance = try runtime.instantiate(module: spectestModule) + func run(spectestModule: Module, configuration: EngineConfiguration, handler: @escaping (TestCase, Location, Result) -> Void) throws { + let engine = Engine(configuration: configuration) + let store = Store(engine: engine) + let spectestInstance = try spectestModule.instantiate(store: store) - try runtime.store.register(hostModuleInstance, as: "spectest") - - var currentInstance: Instance? let rootPath = FilePath(path).removingLastComponent().string var content = content - let context = WastRunContext() + let context = WastRunContext(store: store, rootPath: rootPath) + context.importsSpace.define(module: "spectest", spectestInstance.exports) do { while let (directive, location) = try content.nextDirective() { - directive.run( - runtime: runtime, - context: context, - instance: ¤tInstance, - rootPath: rootPath - ) { command, result in - handler(self, location, result) + do { + if let result = try context.run(directive: directive) { + handler(self, location, result) + } + } catch let error as SpectestError { + handler(self, location, .failed(error.description)) } } } catch let parseError as WatParserError { @@ -142,43 +158,37 @@ extension TestCase { } } -extension WastDirective { - func run( - runtime: Runtime, - context: WastRunContext, - instance currentInstance: inout Instance?, - rootPath: String, - handler: (WastDirective, Result) -> Void - ) { - func instantiate(module: Module, name: String? = nil) throws -> Instance { - let instance = try runtime.instantiate(module: module) - if let name { - context.register(name, instance: instance) +extension WastRunContext { + func instantiate(module: Module, name: String? = nil) throws -> Instance { + let instance = try module.instantiate(store: store, imports: importsSpace) + if let name { + register(name, instance: instance) + } + return instance + } + func deriveModuleInstance(from execute: WastExecute) throws -> Instance? { + switch execute { + case .invoke(let invoke): + if let module = invoke.module { + return lookupInstance(module) + } else { + return currentInstance } + case .wat(var wat): + let module = try parseModule(rootPath: rootPath, moduleSource: .binary(wat.encode())) + let instance = try instantiate(module: module) return instance - } - func deriveModuleInstance(from execute: WastExecute) throws -> Instance? { - switch execute { - case .invoke(let invoke): - if let module = invoke.module { - return context.lookupInstance(module) - } else { - return currentInstance - } - case .wat(var wat): - let module = try parseModule(rootPath: rootPath, moduleSource: .binary(wat.encode())) - let instance = try instantiate(module: module) - return instance - case .get(let module, _): - if let module { - return context.lookupInstance(module) - } else { - return currentInstance - } + case .get(let module, _): + if let module { + return lookupInstance(module) + } else { + return currentInstance } } + } - switch self { + func run(directive: WastDirective) throws -> Result? { + switch directive { case .module(let moduleDirective): currentInstance = nil @@ -186,41 +196,37 @@ extension WastDirective { do { module = try parseModule(rootPath: rootPath, moduleSource: moduleDirective.source) } catch { - return handler(self, .failed("module could not be parsed: \(error)")) + return .failed("module could not be parsed: \(error)") } do { currentInstance = try instantiate(module: module, name: moduleDirective.id) } catch { - return handler(self, .failed("module could not be instantiated: \(error)")) + return .failed("module could not be instantiated: \(error)") } - return handler(self, .passed) + return .passed case .register(let name, let moduleId): - let module: Instance + let instance: Instance if let moduleId { - guard let found = context.lookupInstance(moduleId) else { - return handler(self, .failed("module \(moduleId) not found")) + guard let found = self.lookupInstance(moduleId) else { + return .failed("module \(moduleId) not found") } - module = found + instance = found } else { guard let currentInstance else { - return handler(self, .failed("no current module to register")) + return .failed("no current module to register") } - module = currentInstance - } - - do { - try runtime.store.register(module, as: name) - } catch { - return handler(self, .failed("module could not be registered: \(error)")) + instance = currentInstance } + importsSpace.define(module: name, instance.exports) + return nil case .assertMalformed(let module, let message): currentInstance = nil guard case .binary = module.source else { - return handler(self, .skipped("assert_malformed is only supported for binary modules for now")) + return .skipped("assert_malformed is only supported for binary modules for now") } do { @@ -228,9 +234,9 @@ extension WastDirective { // Materialize all functions to see all errors in the module try module.materializeAll() } catch { - return handler(self, .passed) + return .passed } - return handler(self, .failed("module should not be parsed: expected \"\(message)\"")) + return .failed("module should not be parsed: expected \"\(message)\"") case .assertTrap(execute: .wat(var wat), let message): currentInstance = nil @@ -239,33 +245,33 @@ extension WastDirective { do { module = try parseModule(rootPath: rootPath, moduleSource: .binary(wat.encode())) } catch { - return handler(self, .failed("module could not be parsed: \(error)")) + return .failed("module could not be parsed: \(error)") } do { _ = try instantiate(module: module) } catch let error as InstantiationError { guard error.assertionText.contains(message) else { - return handler(self, .failed("assertion mismatch: expected: \(message), actual: \(error.assertionText)")) + return .failed("assertion mismatch: expected: \(message), actual: \(error.assertionText)") } } catch let error as Trap { guard error.assertionText.contains(message) else { - return handler(self, .failed("assertion mismatch: expected: \(message), actual: \(error.assertionText)")) + return .failed("assertion mismatch: expected: \(message), actual: \(error.assertionText)") } } catch { - return handler(self, .failed("\(error)")) + return .failed("\(error)") } - return handler(self, .passed) + return .passed case .assertReturn(let execute, let results): let instance: Instance? do { instance = try deriveModuleInstance(from: execute) } catch { - return handler(self, .failed("failed to derive module instance: \(error)")) + return .failed("failed to derive module instance: \(error)") } guard let instance else { - return handler(self, .failed("no module to execute")) + return .failed("no module to execute") } let expected = parseValues(args: results) @@ -274,14 +280,14 @@ extension WastDirective { case .invoke(let invoke): let result: [WasmKit.Value] do { - result = try runtime.invoke(instance, function: invoke.name, with: invoke.args) + result = try wastInvoke(call: invoke) } catch { - return handler(self, .failed("\(error)")) + return .failed("\(error)") } guard result.isTestEquivalent(to: expected) else { - return handler(self, .failed("invoke result mismatch: expected: \(expected), actual: \(result)")) + return .failed("invoke result mismatch: expected: \(expected), actual: \(result)") } - return handler(self, .passed) + return .passed case .get(_, let globalName): let result: WasmKit.Value @@ -291,65 +297,43 @@ extension WastDirective { } result = global.value } catch { - return handler(self, .failed("\(error)")) + return .failed("\(error)") } guard result.isTestEquivalent(to: expected[0]) else { - return handler(self, .failed("get result mismatch: expected: \(expected), actual: \(result)")) + return .failed("get result mismatch: expected: \(expected), actual: \(result)") } - return handler(self, .passed) - case .wat: break + return .passed + case .wat: return .skipped("TBD") } case .assertTrap(let execute, let message): - let moduleInstance: Instance? - do { - moduleInstance = try deriveModuleInstance(from: execute) - } catch { - return handler(self, .failed("failed to derive module instance: \(error)")) - } - guard let moduleInstance else { - return handler(self, .failed("no module to execute")) - } - switch execute { case .invoke(let invoke): do { - _ = try runtime.invoke(moduleInstance, function: invoke.name, with: invoke.args) + _ = try wastInvoke(call: invoke) // XXX: This is wrong but just keep it as is - // return handler(self, .failed("trap expected: \(message)")) - return handler(self, .passed) + // return .failed("trap expected: \(message)") + return .passed } catch let trap as Trap { guard trap.assertionText.contains(message) else { - return handler(self, .failed("assertion mismatch: expected: \(message), actual: \(trap.assertionText)")) + return .failed("assertion mismatch: expected: \(message), actual: \(trap.assertionText)") } - return handler(self, .passed) + return .passed } catch { - return handler(self, .failed("\(error)")) + return .failed("\(error)") } default: - return handler(self, .failed("assert_trap is not implemented non-invoke actions")) + return .failed("assert_trap is not implemented non-invoke actions") } case .assertExhaustion(let call, let message): - let moduleInstance: Instance? do { - moduleInstance = try deriveModuleInstance(from: .invoke(call)) - } catch { - return handler(self, .failed("failed to derive module instance: \(error)")) - } - guard let moduleInstance else { - return handler(self, .failed("no module to execute")) - } - - do { - _ = try runtime.invoke(moduleInstance, function: call.name, with: call.args) - return handler(self, .failed("trap expected: \(message)")) + _ = try wastInvoke(call: call) + return .failed("trap expected: \(message)") } catch let trap as Trap { guard trap.assertionText.contains(message) else { - return handler(self, .failed("assertion mismatch: expected: \(message), actual: \(trap.assertionText)")) + return .failed("assertion mismatch: expected: \(message), actual: \(trap.assertionText)") } - return handler(self, .passed) - } catch { - return handler(self, .failed("\(error)")) + return .passed } case .assertUnlinkable(let wat, let message): currentInstance = nil @@ -358,41 +342,43 @@ extension WastDirective { do { module = try parseModule(rootPath: rootPath, moduleSource: .text(wat)) } catch { - return handler(self, .failed("module could not be parsed: \(error)")) + return .failed("module could not be parsed: \(error)") } do { _ = try instantiate(module: module) } catch let error as ImportError { guard error.assertionText.contains(message) else { - return handler(self, .failed("assertion mismatch: expected: \(message), actual: \(error.assertionText)")) + return .failed("assertion mismatch: expected: \(message), actual: \(error.assertionText)") } } catch { - return handler(self, .failed("\(error)")) + return .failed("\(error)") } - return handler(self, .passed) + return .passed case .assertInvalid: - return handler(self, .skipped("validation is no implemented yet")) + return .skipped("validation is no implemented yet") case .invoke(let invoke): - let moduleInstance: Instance? - do { - moduleInstance = try deriveModuleInstance(from: .invoke(invoke)) - } catch { - return handler(self, .failed("failed to derive module instance: \(error)")) - } - guard let moduleInstance else { - return handler(self, .failed("no module to execute")) - } + _ = try wastInvoke(call: invoke) + return .passed + } + } - do { - _ = try runtime.invoke(moduleInstance, function: invoke.name, with: invoke.args) - } catch { - return handler(self, .failed("\(error)")) - } - return handler(self, .passed) + private func wastInvoke(call: WastInvoke) throws -> [Value] { + let instance: Instance? + do { + instance = try deriveModuleInstance(from: .invoke(call)) + } catch { + throw SpectestError("failed to derive module instance: \(error)") + } + guard let instance else { + throw SpectestError("no module to execute") + } + guard let function = instance.exportedFunction(name: call.name) else { + throw SpectestError("function \(call.name) not exported") } + return try function.invoke(call.args) } private func deriveFeatureSet(rootPath: FilePath) -> WasmFeatureSet { diff --git a/Tests/WasmKitTests/SpectestTests.swift b/Tests/WasmKitTests/SpectestTests.swift index 2c7c0ec1..8d5b2940 100644 --- a/Tests/WasmKitTests/SpectestTests.swift +++ b/Tests/WasmKitTests/SpectestTests.swift @@ -17,7 +17,7 @@ final class SpectestTests: XCTestCase { /// Run all the tests in the spectest suite. func testRunAll() async throws { - let defaultConfig = RuntimeConfiguration() + let defaultConfig = EngineConfiguration() let environment = ProcessInfo.processInfo.environment let ok = try await spectest( path: Self.testPaths, @@ -30,7 +30,7 @@ final class SpectestTests: XCTestCase { } func testRunAllWithTokenThreading() async throws { - let defaultConfig = RuntimeConfiguration() + let defaultConfig = EngineConfiguration() guard defaultConfig.threadingModel != .token else { return } // Sanity check that non-default threading models work. var config = defaultConfig diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index c92ea8a6..a3bb4a88 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -61,9 +61,7 @@ enum WasmGen { /// A visitor for WebAssembly instructions. /// /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. - /// See the following parsing functions: - /// - ``parseExpression(bytes:features:hasDataCount:visitor:)`` - /// - ``parseExpression(stream:features:hasDataCount:visitor:)`` + /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { /// The return type of visitor methods.