From ddb56a8a88a3e0fb9e36b803e37cd86cb6ba7653 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 15:48:51 +0900 Subject: [PATCH 01/14] Engine API: Add `Engine` type to replace `Runtime` The new API is intended to allow the following future improvements: - Support multiple `Store` instances in a single `Engine` instance. - Async (suspendable) execution. without breaking the existing API. --- Sources/CLI/Commands/Explore.swift | 2 +- Sources/CLI/Commands/Run.swift | 16 ++-- Sources/WasmKit/CMakeLists.txt | 3 +- Sources/WasmKit/Engine.swift | 83 +++++++++++++++++++ .../WasmKit/Execution/EngineInterceptor.swift | 0 Sources/WasmKit/Execution/Function.swift | 2 +- .../Instructions/InstructionSupport.swift | 2 +- Sources/WasmKit/Execution/Profiler.swift | 2 +- Sources/WasmKit/Execution/Runtime.swift | 40 +-------- .../Execution/RuntimeInterceptor.swift | 29 ------- .../WasmKit/Execution/SignpostTracer.swift | 2 +- Sources/WasmKit/Translator.swift | 14 ++-- 12 files changed, 109 insertions(+), 86 deletions(-) create mode 100644 Sources/WasmKit/Engine.swift create mode 100644 Sources/WasmKit/Execution/EngineInterceptor.swift delete mode 100644 Sources/WasmKit/Execution/RuntimeInterceptor.swift diff --git a/Sources/CLI/Commands/Explore.swift b/Sources/CLI/Commands/Explore.swift index bd8de29a..7497b1c8 100644 --- a/Sources/CLI/Commands/Explore.swift +++ b/Sources/CLI/Commands/Explore.swift @@ -29,7 +29,7 @@ struct Explore: ParsableCommand { hostModuleStubs[importEntry.module] = hostModule } // Instruction dumping requires token threading model for now - let configuration = RuntimeConfiguration(threadingModel: .token) + let configuration = EngineConfiguration(threadingModel: .token) let runtime = Runtime(hostModules: hostModuleStubs, configuration: configuration) let instance = try runtime.instantiate(module: module) var stdout = Stdout() diff --git a/Sources/CLI/Commands/Run.swift b/Sources/CLI/Commands/Run.swift index cd4f2f1a..512daa23 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 @@ -169,7 +169,7 @@ struct Run: ParsableCommand { } } - 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() diff --git a/Sources/WasmKit/CMakeLists.txt b/Sources/WasmKit/CMakeLists.txt index 957e6bad..2905b310 100644 --- a/Sources/WasmKit/CMakeLists.txt +++ b/Sources/WasmKit/CMakeLists.txt @@ -1,4 +1,5 @@ add_wasmkit_library(WasmKit + Engine.swift Module.swift ModuleParser.swift Translator.swift @@ -15,6 +16,7 @@ add_wasmkit_library(WasmKit Execution/Instructions/Misc.swift Execution/Instructions/InstructionSupport.swift Execution/DispatchInstruction.swift + Execution/EngineInterceptor.swift Execution/Errors.swift Execution/Execution.swift Execution/Function.swift @@ -23,7 +25,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/Engine.swift b/Sources/WasmKit/Engine.swift new file mode 100644 index 00000000..8b4d5f93 --- /dev/null +++ b/Sources/WasmKit/Engine.swift @@ -0,0 +1,83 @@ +import _CWasmKit.Platform + +/// A WebAssembly execution engine. +public final class Engine { + 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, interceptor: EngineInterceptor? = nil) { + self.configuration = configuration + self.interceptor = interceptor + self.funcTypeInterner = Interner() + } +} + +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, store: Store) + func onExitFunction(_ function: Function, store: Store) +} + +/// 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, 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) + } + } +} 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/Function.swift b/Sources/WasmKit/Execution/Function.swift index 9635cd57..94f80c7d 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -183,7 +183,7 @@ struct WasmFunctionEntity { let type = self.type var translator = try InstructionTranslator( allocator: runtime.value.store.allocator.iseqAllocator, - runtimeConfiguration: runtime.value.configuration, + engineConfiguration: runtime.value.configuration, funcTypeInterner: runtime.value.funcTypeInterner, module: instance, type: runtime.value.resolveType(type), 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/Profiler.swift b/Sources/WasmKit/Execution/Profiler.swift index 6ca081b3..ae242834 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" diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 5d20986d..09bc52a6 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -1,12 +1,11 @@ import WasmParser -import _CWasmKit.Platform /// A container to manage execution state of one or more module instances. public final class Runtime { public let store: Store - let interceptor: RuntimeInterceptor? + let interceptor: EngineInterceptor? let funcTypeInterner: Interner - let configuration: RuntimeConfiguration + let configuration: EngineConfiguration /// Initializes a new instant of a WebAssembly interpreter runtime. /// - Parameter hostModules: Host module names mapped to their corresponding ``HostModule`` definitions. @@ -14,8 +13,8 @@ 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) @@ -35,37 +34,6 @@ public final class Runtime { } } -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 - } - } - - /// The threading model to use for the virtual machine interpreter. - public var threadingModel: ThreadingModel - - /// Initializes a new instance of `RuntimeConfiguration`. - public init(threadingModel: ThreadingModel? = nil) { - self.threadingModel = threadingModel ?? .defaultForCurrentPlatform - } -} - extension Runtime { public func instantiate(module: Module) throws -> Instance { let instance = try instantiate( 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..87127d11 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 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, From 897c76245c44a9c736a6360b4a197e8d0382ae7a Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 16:00:53 +0900 Subject: [PATCH 02/14] Engine API: Make `Runtime` as a thin wrapper around `Engine` --- Sources/WasmKit/Execution/Runtime.swift | 19 ++++++++++++------- Sources/WasmKit/Execution/Store.swift | 9 +++++++-- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 09bc52a6..b18acc33 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -3,9 +3,16 @@ import WasmParser /// A container to manage execution state of one or more module instances. public final class Runtime { public let store: Store - let interceptor: EngineInterceptor? - let funcTypeInterner: Interner - let configuration: EngineConfiguration + let engine: Engine + var interceptor: EngineInterceptor? { + engine.interceptor + } + var funcTypeInterner: Interner { + engine.funcTypeInterner + } + var configuration: EngineConfiguration { + engine.configuration + } /// Initializes a new instant of a WebAssembly interpreter runtime. /// - Parameter hostModules: Host module names mapped to their corresponding ``HostModule`` definitions. @@ -16,10 +23,8 @@ public final class Runtime { 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) diff --git a/Sources/WasmKit/Execution/Store.swift b/Sources/WasmKit/Execution/Store.swift index 3aa2dc3d..200017a9 100644 --- a/Sources/WasmKit/Execution/Store.swift +++ b/Sources/WasmKit/Execution/Store.swift @@ -41,10 +41,15 @@ public final class Store { /// 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. + let engine: Engine - init(funcTypeInterner: Interner) { - self.allocator = StoreAllocator(funcTypeInterner: funcTypeInterner) + /// Create a new store associated with the given engine. + public init(engine: Engine) { + self.engine = engine + self.allocator = StoreAllocator(funcTypeInterner: engine.funcTypeInterner) } } From f43fb1c144ce14fa7f79682e98c7935ab1d2fb39 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 19:29:46 +0900 Subject: [PATCH 03/14] Engine API: Migrate Execution internals --- Sources/CLI/Commands/Explore.swift | 2 +- Sources/WasmKit/Engine.swift | 21 ++- Sources/WasmKit/Execution/Execution.swift | 38 ++-- Sources/WasmKit/Execution/Function.swift | 62 ++++--- Sources/WasmKit/Execution/Instances.swift | 32 ++-- .../Execution/Instructions/Control.swift | 18 +- .../Execution/Instructions/Memory.swift | 2 +- .../Execution/Instructions/Table.swift | 25 ++- Sources/WasmKit/Execution/Profiler.swift | 8 +- Sources/WasmKit/Execution/Runtime.swift | 162 +++++++++--------- .../WasmKit/Execution/SignpostTracer.swift | 6 +- Sources/WasmKit/Execution/Store.swift | 117 +++---------- .../WasmKit/Execution/StoreAllocator.swift | 14 +- Sources/WasmKit/Module.swift | 101 +++++++++++ .../Execution/HostModuleTests.swift | 9 +- Tests/WasmKitTests/Spectest/Spectest.swift | 2 +- Tests/WasmKitTests/Spectest/TestCase.swift | 6 +- Tests/WasmKitTests/SpectestTests.swift | 4 +- 18 files changed, 336 insertions(+), 293 deletions(-) diff --git a/Sources/CLI/Commands/Explore.swift b/Sources/CLI/Commands/Explore.swift index 7497b1c8..421f2b3b 100644 --- a/Sources/CLI/Commands/Explore.swift +++ b/Sources/CLI/Commands/Explore.swift @@ -33,6 +33,6 @@ struct Explore: ParsableCommand { let runtime = Runtime(hostModules: hostModuleStubs, configuration: configuration) let instance = try runtime.instantiate(module: module) var stdout = Stdout() - try instance.dumpFunctions(to: &stdout, module: module, runtime: runtime) + try instance.dumpFunctions(to: &stdout, module: module) } } diff --git a/Sources/WasmKit/Engine.swift b/Sources/WasmKit/Engine.swift index 8b4d5f93..508f3d2b 100644 --- a/Sources/WasmKit/Engine.swift +++ b/Sources/WasmKit/Engine.swift @@ -54,8 +54,8 @@ public struct EngineConfiguration { @_documentation(visibility: internal) public protocol EngineInterceptor { - func onEnterFunction(_ function: Function, store: Store) - func onExitFunction(_ function: Function, store: Store) + func onEnterFunction(_ function: Function) + func onExitFunction(_ function: Function) } /// An interceptor that multiplexes multiple interceptors @@ -69,15 +69,24 @@ public class MultiplexingInterceptor: EngineInterceptor { self.interceptors = interceptors } - public func onEnterFunction(_ function: Function, store: Store) { + public func onEnterFunction(_ function: Function) { for interceptor in interceptors { - interceptor.onEnterFunction(function, store: store) + interceptor.onEnterFunction(function) } } - public func onExitFunction(_ function: Function, store: Store) { + public func onExitFunction(_ function: Function) { for interceptor in interceptors { - interceptor.onExitFunction(function, store: store) + 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/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 94f80c7d..a2335abb 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -6,11 +6,31 @@ import WasmParser /// public struct Function: Equatable { internal let handle: InternalFunction - let allocator: StoreAllocator + let 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. + 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. + public func callAsFunction(_ arguments: [Value] = []) throws -> [Value] { + return try invoke(arguments) } /// Invokes a function of the given address with the given parameters. @@ -20,9 +40,9 @@ 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(_:)") 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 +95,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 +109,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 +144,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 +186,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, - engineConfiguration: 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..99f2daff 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -88,11 +88,11 @@ typealias InternalInstance = EntityHandle /// 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 +101,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. @@ -117,31 +117,31 @@ public struct Instance { /// A dictionary of exported entities by name. public var exports: Exports { - handle.exports.mapValues { ExternalValue(handle: $0, allocator: allocator) } + handle.exports.mapValues { ExternalValue(handle: $0, store: store) } } /// 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 +149,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) } @@ -443,16 +443,16 @@ 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: allocator)) + self = .table(Table(handle: table, allocator: store.allocator)) case .memory(let memory): - self = .memory(Memory(handle: memory, allocator: allocator)) + self = .memory(Memory(handle: memory, allocator: store.allocator)) case .global(let global): - self = .global(Global(handle: global, allocator: allocator)) + self = .global(Global(handle: global, allocator: store.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/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 ae242834..46a6cbd0 100644 --- a/Sources/WasmKit/Execution/Profiler.swift +++ b/Sources/WasmKit/Execution/Profiler.swift @@ -82,19 +82,19 @@ public class GuestTimeProfiler: EngineInterceptor { } } - 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 b18acc33..96334ee0 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -14,6 +14,12 @@ public final class Runtime { 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. /// - Parameter interceptor: An optional runtime interceptor to intercept execution of instructions. @@ -27,7 +33,7 @@ public final class Runtime { store = Store(engine: engine) for (moduleName, hostModule) in hostModules { - store.registerUniqueHostModule(hostModule, as: moduleName, runtime: self) + registerUniqueHostModule(hostModule, as: moduleName, engine: engine) } } @@ -41,108 +47,96 @@ public final class Runtime { extension Runtime { public func instantiate(module: Module) throws -> Instance { - let instance = try instantiate( - module: module, - externalValues: store.getExternalValues(module, runtime: self) + let instance = try module.instantiate( + store: store, + externalValues: getExternalValues(module, runtime: self) ) - return Instance(handle: instance, allocator: store.allocator) + return Instance(handle: instance, store: store) } - /// > 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 + /// 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) } - // 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 } + availableExports[name] = instance.exports + } - guard isValid else { - throw InstantiationError.importsAndExternalValuesMismatch + /// 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) } - // Steps 5-8. + registerUniqueHostModule(hostModule, as: name, engine: engine) + } - // Step 9. - // Process `elem.init` evaluation during allocation + /// 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]() - // Step 11. - let instance = try store.allocator.allocate( - module: module, runtime: self, - externalValues: externalValues - ) + for (globalName, global) in hostModule.globals { + moduleExports[globalName] = .global(global) + hostGlobals.append(global) + } - 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) + for (functionName, function) in hostModule.functions { + moduleExports[functionName] = .function( + Function( + handle: store.allocator.allocate(hostFunction: function, engine: engine), + store: store + ) + ) + hostFunctions.append(function) } - // 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 - ) - } - } - } catch Trap.undefinedElement, Trap.tableSizeOverflow, Trap.outOfBoundsTableAccess { - throw InstantiationError.outOfBoundsTableAccess - } catch { - throw error + for (memoryName, memoryAddr) in hostModule.memories { + moduleExports[memoryName] = .memory(memoryAddr) } - // 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) - } + availableExports[name] = moduleExports + } + + 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) } - } catch Trap.outOfBoundsMemoryAccess { - throw InstantiationError.outOfBoundsMemoryAccess - } catch { - throw error - } - // Step 17. - if let startIndex = module.start { - let startFunction = try instance.functions[validating: Int(startIndex)] - _ = try startFunction.invoke([], runtime: self) + 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 instance + return result } } @@ -178,7 +172,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") diff --git a/Sources/WasmKit/Execution/SignpostTracer.swift b/Sources/WasmKit/Execution/SignpostTracer.swift index 87127d11..8d46a345 100644 --- a/Sources/WasmKit/Execution/SignpostTracer.swift +++ b/Sources/WasmKit/Execution/SignpostTracer.swift @@ -22,13 +22,13 @@ public class SignpostTracer: EngineInterceptor { "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 200017a9..ac1d4822 100644 --- a/Sources/WasmKit/Execution/Store.swift +++ b/Sources/WasmKit/Execution/Store.swift @@ -26,8 +26,6 @@ public struct HostModule { /// > 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,10 +35,6 @@ 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. @@ -53,6 +47,13 @@ public final class Store { } } +extension Store: Equatable { + public static func == (lhs: Store, rhs: Store) -> Bool { + /// Use reference identity for equality comparison. + return lhs === rhs + } +} + /// A caller context passed to host functions public struct Caller { private let instanceHandle: InternalInstance? @@ -60,18 +61,22 @@ 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 + + /// The runtime that called the host function. + @available(*, unavailable, message: "Use `engine` instead") + public var runtime: Runtime { fatalError() } - init(instanceHandle: InternalInstance?, runtime: Runtime) { + init(instanceHandle: InternalInstance?, store: Store) { self.instanceHandle = instanceHandle - self.runtime = runtime + self.store = store } } @@ -120,52 +125,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 { @@ -176,44 +145,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..05e781e6 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -247,10 +247,10 @@ extension StoreAllocator { /// func allocate( module: Module, - runtime: Runtime, + engine: Engine, + resourceLimiter: any ResourceLimiter, externalValues: [ExternalValue] ) throws -> InternalInstance { - let resourceLimiter = runtime.store.resourceLimiter // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. let types = module.types @@ -301,7 +301,7 @@ extension StoreAllocator { 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) } ) @@ -418,12 +418,12 @@ extension StoreAllocator { 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,10 +431,10 @@ extension StoreAllocator { return InternalFunction.wasm(EntityHandle(unsafe: pointer)) } - func allocate(hostFunction: HostFunction, runtime: Runtime) -> InternalFunction { + func allocate(hostFunction: HostFunction, engine: Engine) -> InternalFunction { let pointer = hostFunctions.allocate( initializing: HostFunctionEntity( - type: runtime.internType(hostFunction.type), + type: engine.internType(hostFunction.type), implementation: hostFunction.implementation ) ) diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 13df0fbb..5a83b826 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -116,6 +116,107 @@ public struct Module { return typeSection[Int(index)] } + public func instantiate(store: Store, imports: HostModule) throws -> Instance { + fatalError("TODO") + } + + /// > Note: + /// + func instantiate(store: Store, externalValues: [ExternalValue]) throws -> InternalInstance { + // Step 3 of instantiation algorithm, according to Wasm 2.0 spec. + guard imports.count == externalValues.count else { + throw InstantiationError.importsAndExternalValuesMismatch + } + + // Step 4. + let isValid = zip(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 } + + guard isValid else { + throw InstantiationError.importsAndExternalValuesMismatch + } + + // 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, + externalValues: externalValues + ) + + 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/Tests/WasmKitTests/Execution/HostModuleTests.swift b/Tests/WasmKitTests/Execution/HostModuleTests.swift index 700b28ee..e7af0111 100644 --- a/Tests/WasmKitTests/Execution/HostModuleTests.swift +++ b/Tests/WasmKitTests/Execution/HostModuleTests.swift @@ -10,7 +10,7 @@ final class HostModuleTests: XCTestCase { let memoryType = MemoryType(min: 1, max: nil) let memory = try runtime.store.allocator.allocate( memoryType: memoryType, resourceLimiter: DefaultResourceLimiter()) - try runtime.store.register( + try runtime.register( HostModule( memories: [ "memory": Memory( @@ -20,8 +20,7 @@ final class HostModuleTests: XCTestCase { ) ] ), - as: "env", - runtime: runtime + as: "env" ) let module = try parseWasm( @@ -67,7 +66,7 @@ final class HostModuleTests: XCTestCase { 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 @@ -77,7 +76,7 @@ final class HostModuleTests: XCTestCase { }, ] ) - try runtime.store.register(hostModule, as: "env", runtime: runtime) + try runtime.register(hostModule, as: "env") let instance = try runtime.instantiate(module: module) // Check foo(wasm) -> bar(host) -> baz(wasm) -> qux(host) _ = try runtime.invoke(instance, function: "foo") 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..b7df4f82 100644 --- a/Tests/WasmKitTests/Spectest/TestCase.swift +++ b/Tests/WasmKitTests/Spectest/TestCase.swift @@ -111,11 +111,11 @@ class WastRunContext { } extension TestCase { - func run(spectestModule: Module, configuration: RuntimeConfiguration, handler: @escaping (TestCase, Location, Result) -> Void) throws { + func run(spectestModule: Module, configuration: EngineConfiguration, handler: @escaping (TestCase, Location, Result) -> Void) throws { let runtime = Runtime(configuration: configuration) let hostModuleInstance = try runtime.instantiate(module: spectestModule) - try runtime.store.register(hostModuleInstance, as: "spectest") + try runtime.register(hostModuleInstance, as: "spectest") var currentInstance: Instance? let rootPath = FilePath(path).removingLastComponent().string @@ -212,7 +212,7 @@ extension WastDirective { } do { - try runtime.store.register(module, as: name) + try runtime.register(module, as: name) } catch { return handler(self, .failed("module could not be registered: \(error)")) } 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 From 54f6b0995bdfe1124645661865eaff23f5da6191 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 20:52:17 +0900 Subject: [PATCH 04/14] Engine API: Add `Module/instantiate(store:imports:)` method --- Sources/WasmKit/Component/CanonicalCall.swift | 4 +- Sources/WasmKit/Execution/Errors.swift | 3 + Sources/WasmKit/Execution/Function.swift | 35 ++++++++ Sources/WasmKit/Execution/Instances.swift | 13 +++ Sources/WasmKit/Execution/Runtime.swift | 82 ++++++++++++++++--- Sources/WasmKit/Execution/Store.swift | 61 -------------- .../WasmKit/Execution/StoreAllocator.swift | 64 ++++++++++----- Sources/WasmKit/Imports.swift | 33 ++++++++ Sources/WasmKit/Module.swift | 35 +++----- 9 files changed, 211 insertions(+), 119 deletions(-) create mode 100644 Sources/WasmKit/Imports.swift diff --git a/Sources/WasmKit/Component/CanonicalCall.swift b/Sources/WasmKit/Component/CanonicalCall.swift index 7abbec54..a5f4d1d2 100644 --- a/Sources/WasmKit/Component/CanonicalCall.swift +++ b/Sources/WasmKit/Component/CanonicalCall.swift @@ -37,8 +37,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") 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/Function.swift b/Sources/WasmKit/Execution/Function.swift index a2335abb..14b5554a 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. /// @@ -8,6 +9,40 @@ public struct Function: Equatable { internal let handle: InternalFunction 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 { store.allocator.funcTypeInterner.resolve(handle.type) diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 99f2daff..b625b3f0 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -455,6 +455,19 @@ public enum ExternalValue: Equatable { 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): + return (.table(table.handle), table.allocator) + case .memory(let memory): + return (.memory(memory.handle), memory.allocator) + case .global(let global): + return (.global(global.handle), global.allocator) + } + } } enum InternalExternalValue { diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 96334ee0..0df47143 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -47,12 +47,10 @@ public final class Runtime { extension Runtime { public func instantiate(module: Module) throws -> Instance { - let instance = try module.instantiate( + return try module.instantiate( store: store, - externalValues: getExternalValues(module, runtime: self) + imports: getExternalValues(module, runtime: self) ) - - return Instance(handle: instance, store: store) } /// Legacy compatibility method to register a module instance with a name. @@ -85,7 +83,7 @@ extension Runtime { for (functionName, function) in hostModule.functions { moduleExports[functionName] = .function( Function( - handle: store.allocator.allocate(hostFunction: function, engine: engine), + handle: store.allocator.allocate(type: function.type, implementation: function.implementation, engine: engine), store: store ) ) @@ -99,8 +97,8 @@ extension Runtime { availableExports[name] = moduleExports } - func getExternalValues(_ module: Module, runtime: Runtime) throws -> [ExternalValue] { - var result = [ExternalValue]() + func getExternalValues(_ module: Module, runtime: Runtime) throws -> Imports { + var result = Imports() for i in module.imports { guard let moduleExports = availableExports[i.module], let external = moduleExports[i.name] else { @@ -113,23 +111,23 @@ extension Runtime { guard runtime.internType(module.types[Int(typeIndex)]) == type else { throw ImportError.incompatibleImportType } - result.append(external) + result.define(i, external) case let (.table(tableType), .table(table)): if let max = table.handle.limits.max, max < tableType.limits.min { throw ImportError.incompatibleImportType } - result.append(external) + result.define(i, external) case let (.memory(memoryType), .memory(memory)): if let max = memory.handle.limit.max, max < memoryType.min { throw ImportError.incompatibleImportType } - result.append(external) + result.define(i, external) case let (.global(globalType), .global(global)) where globalType == global.handle.globalType: - result.append(external) + result.define(i, external) default: throw ImportError.incompatibleImportType @@ -181,6 +179,68 @@ extension Runtime { } } +/// 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 { + // @available(*, deprecated, renamed: "Function.init(store:type:implementation:)") + 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] +} + +/// 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] +} + protocol ConstEvaluationContextProtocol { func functionRef(_ index: FunctionIndex) throws -> Reference func globalValue(_ index: GlobalIndex) throws -> Value diff --git a/Sources/WasmKit/Execution/Store.swift b/Sources/WasmKit/Execution/Store.swift index ac1d4822..4dce7b2a 100644 --- a/Sources/WasmKit/Execution/Store.swift +++ b/Sources/WasmKit/Execution/Store.swift @@ -1,27 +1,5 @@ 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: /// @@ -80,45 +58,6 @@ public struct Caller { } } -/// 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 - } - - public let type: FunctionType - public let implementation: (Caller, [Value]) throws -> [Value] -} - struct HostFunctionEntity { let type: InternedFuncType let implementation: (Caller, [Value]) throws -> [Value] diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index 05e781e6..79d01f43 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -249,7 +249,7 @@ extension StoreAllocator { module: Module, engine: Engine, resourceLimiter: any ResourceLimiter, - externalValues: [ExternalValue] + imports: Imports ) throws -> InternalInstance { // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. @@ -264,20 +264,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 } } @@ -413,8 +435,7 @@ extension StoreAllocator { /// > Note: /// - /// TODO: Mark as private - func allocate( + private func allocate( function: GuestFunction, index: FunctionIndex, instance: InternalInstance, @@ -431,11 +452,14 @@ extension StoreAllocator { return InternalFunction.wasm(EntityHandle(unsafe: pointer)) } - func allocate(hostFunction: HostFunction, engine: Engine) -> InternalFunction { + internal func allocate( + type: FunctionType, + implementation: @escaping (Caller, [Value]) throws -> [Value], + engine: Engine + ) -> InternalFunction { let pointer = hostFunctions.allocate( initializing: HostFunctionEntity( - type: engine.internType(hostFunction.type), - implementation: hostFunction.implementation + type: engine.internType(type), implementation: implementation ) ) return InternalFunction.host(EntityHandle(unsafe: pointer)) diff --git a/Sources/WasmKit/Imports.swift b/Sources/WasmKit/Imports.swift new file mode 100644 index 00000000..7804c204 --- /dev/null +++ b/Sources/WasmKit/Imports.swift @@ -0,0 +1,33 @@ +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: ExternalValue) { + definitions[module, default: [:]][name] = value + } + + 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() + } +} + +extension Imports: ExpressibleByDictionaryLiteral { + public typealias Key = String + public typealias Value = [String: ExternalValue] + + public init(dictionaryLiteral elements: (String, [String: ExternalValue])...) { + self.definitions = Dictionary(uniqueKeysWithValues: elements) + } +} diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 5a83b826..ab1151a4 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -116,34 +116,19 @@ public struct Module { return typeSection[Int(index)] } - public func instantiate(store: Store, imports: HostModule) throws -> Instance { - fatalError("TODO") + /// 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: /// - func instantiate(store: Store, externalValues: [ExternalValue]) throws -> InternalInstance { - // Step 3 of instantiation algorithm, according to Wasm 2.0 spec. - guard imports.count == externalValues.count else { - throw InstantiationError.importsAndExternalValuesMismatch - } - - // Step 4. - let isValid = zip(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 } - - guard isValid else { - throw InstantiationError.importsAndExternalValuesMismatch - } - + private func instantiateHandle(store: Store, imports: Imports) throws -> InternalInstance { // Steps 5-8. // Step 9. @@ -153,7 +138,7 @@ public struct Module { let instance = try store.allocator.allocate( module: self, engine: store.engine, resourceLimiter: store.resourceLimiter, - externalValues: externalValues + imports: imports ) if let nameSection = customSections.first(where: { $0.name == "name" }) { From dc824d7a4a8b804cba29b06a165a083ba143e3c9 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 21:44:37 +0900 Subject: [PATCH 05/14] Engine API: Migrate Spectest suite to use Engine --- Sources/WasmKit/Execution/Function.swift | 3 + Sources/WasmKit/Imports.swift | 44 +++- Sources/WasmKit/Module.swift | 2 +- Tests/WasmKitTests/Spectest/TestCase.swift | 248 ++++++++++----------- 4 files changed, 162 insertions(+), 135 deletions(-) diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 14b5554a..de5c7f1a 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -54,6 +54,7 @@ public struct Function: Equatable { /// - 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) } @@ -64,6 +65,7 @@ public struct Function: Equatable { /// - 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) } @@ -76,6 +78,7 @@ public struct Function: Equatable { /// - 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] { return try invoke(arguments) } diff --git a/Sources/WasmKit/Imports.swift b/Sources/WasmKit/Imports.swift index 7804c204..bc029300 100644 --- a/Sources/WasmKit/Imports.swift +++ b/Sources/WasmKit/Imports.swift @@ -13,6 +13,14 @@ public struct Imports { definitions[module, default: [:]][name] = value } + /// 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: Instance.Exports) { + definitions[module, default: [:]].merge(values, uniquingKeysWith: { _, new in new }) + } + mutating func define(_ importEntry: Import, _ value: ExternalValue) { define(module: importEntry.module, name: importEntry.name, value) } @@ -23,11 +31,41 @@ public struct Imports { } } +/// A value that can be imported or exported from an instance. +public protocol ExternalValueConvertible { + var externalValue: ExternalValue { get } +} + +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 typealias Value = [String: ExternalValue] + 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, [String: ExternalValue])...) { - self.definitions = Dictionary(uniqueKeysWithValues: elements) + 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 ab1151a4..af789fd9 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -122,7 +122,7 @@ public struct Module { /// - 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 { + public func instantiate(store: Store, imports: Imports = [:]) throws -> Instance { Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) } diff --git a/Tests/WasmKitTests/Spectest/TestCase.swift b/Tests/WasmKitTests/Spectest/TestCase.swift index b7df4f82..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] } @@ -112,24 +130,22 @@ class WastRunContext { extension TestCase { func run(spectestModule: Module, configuration: EngineConfiguration, handler: @escaping (TestCase, Location, Result) -> Void) throws { - let runtime = Runtime(configuration: configuration) - let hostModuleInstance = try runtime.instantiate(module: spectestModule) + let engine = Engine(configuration: configuration) + let store = Store(engine: engine) + let spectestInstance = try spectestModule.instantiate(store: store) - try runtime.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.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 { From 526359796d3845f13837ece3aa08adf6e375378d Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 22:19:06 +0900 Subject: [PATCH 06/14] Engine API: Migrate CLI module --- Sources/CLI/Commands/Explore.swift | 24 ++++---- Sources/CLI/Commands/Run.swift | 20 +++++-- Sources/WasmKit/Engine.swift | 4 ++ Sources/WasmKit/Execution/Instances.swift | 59 ++++++++++++++++++- Sources/WasmKit/Execution/Runtime.swift | 3 +- Sources/WasmKit/Imports.swift | 10 +++- .../WASIBridgeToHost+WasmKit.swift | 30 ++++++++-- 7 files changed, 122 insertions(+), 28 deletions(-) diff --git a/Sources/CLI/Commands/Explore.swift b/Sources/CLI/Commands/Explore.swift index 421f2b3b..f6d01c5c 100644 --- a/Sources/CLI/Commands/Explore.swift +++ b/Sources/CLI/Commands/Explore.swift @@ -14,24 +14,28 @@ 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 = EngineConfiguration(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) } diff --git a/Sources/CLI/Commands/Run.swift b/Sources/CLI/Commands/Run.swift index 512daa23..f5d4f0ef 100644 --- a/Sources/CLI/Commands/Run.swift +++ b/Sources/CLI/Commands/Run.swift @@ -161,10 +161,13 @@ 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)) } } @@ -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/WasmKit/Engine.swift b/Sources/WasmKit/Engine.swift index 508f3d2b..75c864ea 100644 --- a/Sources/WasmKit/Engine.swift +++ b/Sources/WasmKit/Engine.swift @@ -16,6 +16,10 @@ public final class Engine { 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() } } public struct EngineConfiguration { diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index b625b3f0..650f9434 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -82,6 +82,61 @@ struct InstanceEntity /* : ~Copyable */ { typealias InternalInstance = EntityHandle +/// A map of exported entities by name. +public struct Exports: Sequence { + let store: Store + let values: [String: InternalExternalValue] + + /// Returns the exported entity with the given name. + public subscript(_ name: String) -> ExternalValue? { + guard let entity = values[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.values.makeIterator() + } + + public mutating func next() -> (String, 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:)``. /// > Note: @@ -113,11 +168,9 @@ 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, store: store) } + Exports(store: store, values: handle.exports) } /// Dumps the textual representation of all functions in the instance. diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 0df47143..40446f44 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -24,6 +24,7 @@ public final class Runtime { /// - Parameter hostModules: Host module names mapped to their corresponding ``HostModule`` definitions. /// - Parameter interceptor: An optional runtime interceptor to intercept execution of instructions. /// - Parameter configuration: An optional runtime configuration to customize the runtime behavior. + @available(*, deprecated) public init( hostModules: [String: HostModule] = [:], interceptor: EngineInterceptor? = nil, @@ -59,7 +60,7 @@ extension Runtime { throw ImportError.moduleInstanceAlreadyRegistered(name) } - availableExports[name] = instance.exports + availableExports[name] = Dictionary(uniqueKeysWithValues: instance.exports) } /// Legacy compatibility method to register a host module with a name. diff --git a/Sources/WasmKit/Imports.swift b/Sources/WasmKit/Imports.swift index bc029300..4bc10cf5 100644 --- a/Sources/WasmKit/Imports.swift +++ b/Sources/WasmKit/Imports.swift @@ -9,15 +9,15 @@ public struct Imports { } /// Define a value to be imported by the given module and name. - public mutating func define(module: String, name: String, _ value: ExternalValue) { - definitions[module, default: [:]][name] = value + 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: Instance.Exports) { + public mutating func define(module: String, _ values: Exports) { definitions[module, default: [:]].merge(values, uniquingKeysWith: { _, new in new }) } @@ -36,6 +36,10 @@ public protocol ExternalValueConvertible { var externalValue: ExternalValue { get } } +extension ExternalValue: ExternalValueConvertible { + public var externalValue: ExternalValue { self } +} + extension Memory: ExternalValueConvertible { public var externalValue: ExternalValue { .memory(self) } } diff --git a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift index 0c4d6377..233b80df 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) { + var imports = Imports() + 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)) + ) + } + } + } + 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,19 @@ 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 } + + public func start(_ instance: Instance, runtime: Runtime) throws -> UInt32 { + return try start(instance) + } } From 5854adfc7614819d89c768c1d154cf2df341fb1e Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 22:23:21 +0900 Subject: [PATCH 07/14] Fix CMake build --- Sources/WasmKit/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/WasmKit/CMakeLists.txt b/Sources/WasmKit/CMakeLists.txt index 2905b310..40388fc9 100644 --- a/Sources/WasmKit/CMakeLists.txt +++ b/Sources/WasmKit/CMakeLists.txt @@ -1,5 +1,6 @@ add_wasmkit_library(WasmKit Engine.swift + Imports.swift Module.swift ModuleParser.swift Translator.swift From cb71fdfb63ebf8716da5f4346a071e2670b28f36 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 22:23:31 +0900 Subject: [PATCH 08/14] Engine API: Split const evaluation into a separate file This is a preparation for the future removal of Runtime.swift. --- Sources/WasmKit/CMakeLists.txt | 1 + .../WasmKit/Execution/ConstEvaluation.swift | 80 +++++++++++++++++++ Sources/WasmKit/Execution/Runtime.swift | 79 ------------------ 3 files changed, 81 insertions(+), 79 deletions(-) create mode 100644 Sources/WasmKit/Execution/ConstEvaluation.swift diff --git a/Sources/WasmKit/CMakeLists.txt b/Sources/WasmKit/CMakeLists.txt index 40388fc9..f7594f06 100644 --- a/Sources/WasmKit/CMakeLists.txt +++ b/Sources/WasmKit/CMakeLists.txt @@ -16,6 +16,7 @@ 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 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/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 40446f44..9c4ae77a 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -241,82 +241,3 @@ public struct HostModule { /// Names of functions exported by this module mapped to corresponding host functions. public var functions: [String: HostFunction] } - -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)") - } - } - } -} From b946b8efbbf344f0083d92d654c0d21b7ff9ba91 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 2 Oct 2024 01:36:29 +0900 Subject: [PATCH 09/14] Engine API: Migrate WITOverlayGenerator --- .../HostGenerators/HostExportFunction.swift | 13 +- Sources/WasmKit/Component/CanonicalCall.swift | 19 +-- .../WasmKit/Component/CanonicalOptions.swift | 8 +- Sources/WasmKit/Execution/Runtime.swift | 6 +- .../WASIBridgeToHost+WasmKit.swift | 1 + .../Runtime/RuntimeSmokeTests.swift | 2 +- .../Runtime/RuntimeTypesTests.swift | 118 +++++++++--------- 7 files changed, 86 insertions(+), 81 deletions(-) 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/WasmKit/Component/CanonicalCall.swift b/Sources/WasmKit/Component/CanonicalCall.swift index a5f4d1d2..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. @@ -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/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 9c4ae77a..c1050271 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -1,6 +1,7 @@ import WasmParser /// 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 engine: Engine @@ -24,7 +25,6 @@ public final class Runtime { /// - Parameter hostModules: Host module names mapped to their corresponding ``HostModule`` definitions. /// - Parameter interceptor: An optional runtime interceptor to intercept execution of instructions. /// - Parameter configuration: An optional runtime configuration to customize the runtime behavior. - @available(*, deprecated) public init( hostModules: [String: HostModule] = [:], interceptor: EngineInterceptor? = nil, @@ -44,9 +44,7 @@ public final class Runtime { func internType(_ type: FunctionType) -> InternedFuncType { return funcTypeInterner.intern(type) } -} -extension Runtime { public func instantiate(module: Module) throws -> Instance { return try module.instantiate( store: store, @@ -137,9 +135,7 @@ extension Runtime { return result } -} -extension Runtime { @available(*, unavailable, message: "Runtime doesn't manage execution state anymore. Use Execution.step instead") public func step() throws { fatalError() diff --git a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift index 233b80df..537e528b 100644 --- a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift +++ b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift @@ -47,6 +47,7 @@ extension WASIBridgeToHost { 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/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift index b4990e95..87d73ca7 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift @@ -8,7 +8,7 @@ class RuntimeSmokeTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "Smoke") try harness.build(link: SmokeTestWorld.link(_:)) { (runtime, instance) in let component = SmokeTestWorld(instance: instance) - _ = try component.hello(runtime: runtime) + _ = try component.hello() } } } diff --git a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift index cee5de21..2a3c61dc 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift @@ -10,63 +10,63 @@ class RuntimeTypesTests: XCTestCase { try harness.build(link: NumberTestWorld.link(_:)) { (runtime, 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) } @@ -78,7 +78,7 @@ class RuntimeTypesTests: XCTestCase { 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) } } } @@ -87,23 +87,23 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "Option") try harness.build(link: OptionTestWorld.link(_:)) { (runtime, 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) } } @@ -113,15 +113,15 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "Record") try harness.build(link: RecordTestWorld.link(_:)) { (runtime, 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) } @@ -131,12 +131,12 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "String") try harness.build(link: StringTestWorld.link(_:)) { (runtime, 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) } } @@ -144,14 +144,14 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "List") try harness.build(link: ListTestWorld.link(_:)) { (runtime, 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) } } @@ -159,22 +159,22 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "Variant") try harness.build(link: VariantTestWorld.link(_:)) { (runtime, 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 @@ -188,29 +188,29 @@ class RuntimeTypesTests: XCTestCase { try harness.build(link: ResultTestWorld.link(_:)) { (runtime, 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"))) } } @@ -220,15 +220,15 @@ class RuntimeTypesTests: XCTestCase { try harness.build(link: EnumTestWorld.link(_:)) { (runtime, 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) } @@ -239,22 +239,22 @@ class RuntimeTypesTests: XCTestCase { try harness.build(link: FlagsTestWorld.link(_:)) { (runtime, 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) } } @@ -262,7 +262,7 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "Tuple") try harness.build(link: TupleTestWorld.link(_:)) { (runtime, 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) } @@ -272,11 +272,11 @@ class RuntimeTypesTests: XCTestCase { var harness = try RuntimeTestHarness(fixture: "Interface") try harness.build(link: InterfaceTestWorld.link(_:)) { (runtime, 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) } } From f0c1ea8f8590163d0a28115dd09d5313150e39e3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 2 Oct 2024 01:57:33 +0900 Subject: [PATCH 10/14] Engine API: Migrate all Runtime usage in ./Tests --- .../HostGenerators/HostWorldGenerator.swift | 2 +- Sources/WasmKit/Engine.swift | 2 +- Sources/WasmKit/Execution/Instances.swift | 13 ++++++ .../WASIBridgeToHost+WasmKit.swift | 1 - Tests/WASITests/IntegrationTests.swift | 9 ++-- .../Runtime/RuntimeSmokeTests.swift | 2 +- .../Runtime/RuntimeTestHarness.swift | 17 ++++---- .../Runtime/RuntimeTypesTests.swift | 26 ++++++------ .../Execution/HostModuleTests.swift | 42 ++++++++----------- .../Runtime/StoreAllocatorTests.swift | 7 ++-- Tests/WasmKitTests/ExecutionTests.swift | 16 ++++--- 11 files changed, 76 insertions(+), 61 deletions(-) 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/Engine.swift b/Sources/WasmKit/Engine.swift index 75c864ea..c1489eb8 100644 --- a/Sources/WasmKit/Engine.swift +++ b/Sources/WasmKit/Engine.swift @@ -11,7 +11,7 @@ public final class Engine { /// - Parameters: /// - configuration: The engine configuration. /// - interceptor: An optional runtime interceptor to intercept execution of instructions. - public init(configuration: EngineConfiguration, interceptor: EngineInterceptor? = nil) { + public init(configuration: EngineConfiguration = EngineConfiguration(), interceptor: EngineInterceptor? = nil) { self.configuration = configuration self.interceptor = interceptor self.funcTypeInterner = Interner() diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 650f9434..86a34f12 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -367,6 +367,19 @@ 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 diff --git a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift index 537e528b..bd9593c9 100644 --- a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift +++ b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift @@ -5,7 +5,6 @@ public typealias WASIBridgeToHost = WASI.WASIBridgeToHost extension WASIBridgeToHost { public func link(to imports: inout Imports, store: Store) { - var imports = Imports() for (moduleName, module) in wasiHostModules { for (name, function) in module.functions { imports.define( 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 87d73ca7..6e8e0334 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeSmokeTests.swift @@ -6,7 +6,7 @@ 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() } 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 2a3c61dc..5f6fe490 100644 --- a/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift +++ b/Tests/WITOverlayGeneratorTests/Runtime/RuntimeTypesTests.swift @@ -7,7 +7,7 @@ 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(v: true), true) @@ -74,7 +74,7 @@ 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 { @@ -85,7 +85,7 @@ class RuntimeTypesTests: XCTestCase { 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() XCTAssertEqual(value1, nil) @@ -111,7 +111,7 @@ 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() @@ -129,7 +129,7 @@ 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(), "") XCTAssertEqual(try component.roundtrip(v: "ok"), "ok") @@ -142,7 +142,7 @@ class RuntimeTypesTests: XCTestCase { 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(), []) for value in [[], [1, 2, 3]] as [[UInt8]] { @@ -157,7 +157,7 @@ class RuntimeTypesTests: XCTestCase { 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(), .a(33_550_336)) @@ -185,7 +185,7 @@ 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(v: .success(())) @@ -217,7 +217,7 @@ class RuntimeTypesTests: XCTestCase { 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(v: .a) @@ -236,7 +236,7 @@ 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(v: []), []) @@ -260,7 +260,7 @@ class RuntimeTypesTests: XCTestCase { 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(v: (true, 42)) XCTAssertEqual(value1.0, true) @@ -270,7 +270,7 @@ 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(v: 42) XCTAssertEqual(value1, 42) @@ -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 e7af0111..ad30e170 100644 --- a/Tests/WasmKitTests/Execution/HostModuleTests.swift +++ b/Tests/WasmKitTests/Execution/HostModuleTests.swift @@ -6,22 +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.register( - HostModule( - memories: [ - "memory": Memory( - handle: memory, - allocator: runtime.store - .allocator - ) - ] - ), - as: "env" - ) + let memory = try WasmKit.Memory(store: store, type: memoryType) + let imports: Imports = [ + "env": ["memory": memory] + ] let module = try parseWasm( bytes: wat2wasm( @@ -30,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( @@ -58,9 +50,9 @@ 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 @@ -69,17 +61,17 @@ final class HostModuleTests: XCTestCase { _ = try foo() return [] }, - "qux": HostFunction(type: voidSignature) { _, _ in + "qux": Function(store: store, type: voidSignature) { caller, _ in XCTAssertTrue(isExecutingFoo) isQuxCalled = true return [] }, ] - ) - try runtime.register(hostModule, as: "env") - 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)]) } } From f2e60ab99417f919fbfab3f794919c048a2506b0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 2 Oct 2024 02:05:30 +0900 Subject: [PATCH 11/14] Engine API: Migrate examples to the new API --- Examples/Sources/Factorial/Factorial.swift | 8 ++-- Examples/Sources/PrintAdd/PrintAdd.swift | 32 ++++++++++------ Examples/Sources/WASI-Hello/Hello.swift | 12 ++++-- Sources/WasmKit/Docs.docc/Docs.md | 44 ++++++++++++++-------- 4 files changed, 61 insertions(+), 35 deletions(-) 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/Sources/WasmKit/Docs.docc/Docs.md b/Sources/WasmKit/Docs.docc/Docs.md index 4844df4b..9ead0e82 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)) ``` From b667d3bf62abc58723ffb83ed032ad34cf261353 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 2 Oct 2024 02:21:37 +0900 Subject: [PATCH 12/14] Engine API: Migrate fuzzing targets --- .../FuzzDifferential/FuzzDifferential.swift | 9 +++++---- FuzzTesting/Sources/FuzzExecute/FuzzExecute.swift | 9 +++++---- Sources/WasmKit/Execution/Instances.swift | 15 ++++++++++----- Sources/WasmKit/Execution/Runtime.swift | 2 +- Sources/WasmKit/Imports.swift | 2 +- 5 files changed, 22 insertions(+), 15 deletions(-) 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/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 86a34f12..2508ff42 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -85,11 +85,16 @@ typealias InternalInstance = EntityHandle /// A map of exported entities by name. public struct Exports: Sequence { let store: Store - let values: [String: InternalExternalValue] + 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 = values[name] else { return nil } + guard let entity = items[name] else { return nil } return ExternalValue(handle: entity, store: store) } @@ -123,10 +128,10 @@ public struct Exports: Sequence { init(parent: Exports) { self.store = parent.store - self.iterator = parent.values.makeIterator() + self.iterator = parent.items.makeIterator() } - public mutating func next() -> (String, ExternalValue)? { + public mutating func next() -> (name: String, value: ExternalValue)? { guard let (name, entity) = iterator.next() else { return nil } return (name, ExternalValue(handle: entity, store: store)) } @@ -170,7 +175,7 @@ public struct Instance { /// A dictionary of exported entities by name. public var exports: Exports { - Exports(store: store, values: handle.exports) + Exports(store: store, items: handle.exports) } /// Dumps the textual representation of all functions in the instance. diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index c1050271..94f80dd1 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -58,7 +58,7 @@ public final class Runtime { throw ImportError.moduleInstanceAlreadyRegistered(name) } - availableExports[name] = Dictionary(uniqueKeysWithValues: instance.exports) + availableExports[name] = Dictionary(uniqueKeysWithValues: instance.exports.map { ($0, $1) }) } /// Legacy compatibility method to register a host module with a name. diff --git a/Sources/WasmKit/Imports.swift b/Sources/WasmKit/Imports.swift index 4bc10cf5..8e8afdaf 100644 --- a/Sources/WasmKit/Imports.swift +++ b/Sources/WasmKit/Imports.swift @@ -18,7 +18,7 @@ public struct Imports { /// - 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, uniquingKeysWith: { _, new in new }) + definitions[module, default: [:]].merge(values.map { ($0, $1) }, uniquingKeysWith: { _, new in new }) } mutating func define(_ importEntry: Import, _ value: ExternalValue) { From e3f8a4039e0deb5b3e6998035d6906d8f7dfe8d6 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 2 Oct 2024 02:49:43 +0900 Subject: [PATCH 13/14] Engine API: Minor documentation updates and add accessors for entities --- Sources/WasmKit/Docs.docc/Docs.md | 14 ++++++--- Sources/WasmKit/Engine.swift | 6 ++++ Sources/WasmKit/Execution/Instances.swift | 31 ++++++++++++++++++- Sources/WasmKit/Execution/Runtime.swift | 4 ++- Sources/WasmKit/Execution/Store.swift | 2 +- .../WasmKit/Execution/StoreAllocator.swift | 2 +- .../WASIBridgeToHost+WasmKit.swift | 1 + Sources/WasmParser/Docs.docc/Docs.md | 1 - Sources/WasmParser/InstructionVisitor.swift | 4 +-- Utilities/Sources/WasmGen.swift | 4 +-- 10 files changed, 54 insertions(+), 15 deletions(-) diff --git a/Sources/WasmKit/Docs.docc/Docs.md b/Sources/WasmKit/Docs.docc/Docs.md index 9ead0e82..900bdb8e 100644 --- a/Sources/WasmKit/Docs.docc/Docs.md +++ b/Sources/WasmKit/Docs.docc/Docs.md @@ -89,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 index c1489eb8..bc77fe94 100644 --- a/Sources/WasmKit/Engine.swift +++ b/Sources/WasmKit/Engine.swift @@ -1,7 +1,12 @@ 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 @@ -22,6 +27,7 @@ public final class Engine { 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. diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 2508ff42..ceeba5fe 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -143,7 +143,7 @@ public struct Exports: Sequence { } /// A stateful instance of a WebAssembly module. -/// Usually instantiated by ``Runtime/instantiate(module:)``. +/// Usually instantiated by ``Module/instantiate(store:imports:)``. /// > Note: /// public struct Instance { @@ -310,6 +310,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 */ { @@ -389,6 +413,11 @@ public struct Memory: Equatable { public var data: [UInt8] { handle.data } + + /// The type of the memory instance. + public var type: MemoryType { + handle.limit + } } extension Memory: GuestMemory { diff --git a/Sources/WasmKit/Execution/Runtime.swift b/Sources/WasmKit/Execution/Runtime.swift index 94f80dd1..8d3710f8 100644 --- a/Sources/WasmKit/Execution/Runtime.swift +++ b/Sources/WasmKit/Execution/Runtime.swift @@ -205,8 +205,9 @@ public final class Runtime { /// return [] /// } /// ``` +@available(*, deprecated, renamed: "Function", message: "`HostFunction` is now unified with `Function`") public struct HostFunction { - // @available(*, deprecated, renamed: "Function.init(store:type:implementation:)") + @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 @@ -217,6 +218,7 @@ public struct HostFunction { } /// 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] = [:], diff --git a/Sources/WasmKit/Execution/Store.swift b/Sources/WasmKit/Execution/Store.swift index 4dce7b2a..303ad28c 100644 --- a/Sources/WasmKit/Execution/Store.swift +++ b/Sources/WasmKit/Execution/Store.swift @@ -16,7 +16,7 @@ public final class Store { /// The allocator allocating and retaining resources for this store. let allocator: StoreAllocator /// The engine associated with this store. - let engine: Engine + public let engine: Engine /// Create a new store associated with the given engine. public init(engine: Engine) { diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index 79d01f43..fc8c6f04 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -467,7 +467,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/WasmKitWASI/WASIBridgeToHost+WasmKit.swift b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift index bd9593c9..67de2d90 100644 --- a/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift +++ b/Sources/WasmKitWASI/WASIBridgeToHost+WasmKit.swift @@ -16,6 +16,7 @@ extension WASIBridgeToHost { } } + @available(*, deprecated, renamed: "link(to:store:)", message: "Use `Engine`-based API instead") public var hostModules: [String: HostModule] { wasiHostModules.mapValues { (module: WASIHostModule) -> HostModule in HostModule( 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/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. From cb9f25f34e3809cac8b3862179b3e41c17a56fc1 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 1 Oct 2024 18:46:51 +0000 Subject: [PATCH 14/14] Fix deinit against uninited InstanceEntity --- Sources/WasmKit/Execution/Instances.swift | 15 ++++++++++++ .../WasmKit/Execution/StoreAllocator.swift | 23 ++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index ceeba5fe..f12aad6a 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -78,6 +78,21 @@ 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 diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index fc8c6f04..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] @@ -254,9 +259,6 @@ extension StoreAllocator { // 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] = [] @@ -318,6 +320,20 @@ 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, @@ -430,6 +446,7 @@ extension StoreAllocator { hasDataCount: module.hasDataCount ) instancePointer.initialize(to: instanceEntity) + instanceInitialized = true return instanceHandle }