diff --git a/wasm/CHANGELOG.md b/wasm/CHANGELOG.md index e148ac3..0f58aa2 100644 --- a/wasm/CHANGELOG.md +++ b/wasm/CHANGELOG.md @@ -4,6 +4,8 @@ - All WASM modules and isntances use a singleton store, to enable sharing of memory and functions. - Add options to setup.dart for configuring the build. +- Add Module.serialize and Module.deserialize. +- Add bin/precompile.dart, which compiles and serializes wasm modules. ## 0.1.0+1 diff --git a/wasm/bin/module.g.def b/wasm/bin/module.g.def index 0f95883..0007d0c 100644 --- a/wasm/bin/module.g.def +++ b/wasm/bin/module.g.def @@ -82,9 +82,11 @@ EXPORTS wasm_memorytype_delete wasm_memorytype_new wasm_module_delete + wasm_module_deserialize wasm_module_exports wasm_module_imports wasm_module_new + wasm_module_serialize wasm_store_delete wasm_store_new wasm_trap_delete diff --git a/wasm/bin/precompile.dart b/wasm/bin/precompile.dart new file mode 100644 index 0000000..4b8926b --- /dev/null +++ b/wasm/bin/precompile.dart @@ -0,0 +1,61 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Precompiles and serializes a wasm module, so that it can be deserialized at +// runtime using WasmModule.deserialize. This skips doing the compilation at +// runtime, decreasing startup times. +// Usage: dart run wasm:precompile wasm_module.wasm -o serialized + +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:wasm/wasm.dart'; + +void main(List arguments) { + final parser = ArgParser() + ..addOption( + 'out', + abbr: 'o', + help: 'Output file.', + ) + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Show this help.', + ); + final args = parser.parse(arguments); + + if (args['help'] as bool) { + print('Usage: dart run wasm:precompile wasm_module.wasm -o serialized\n'); + print(parser.usage); + return; + } + + if (args.rest.isEmpty) { + print('No input file.'); + exitCode = 1; + return; + } + + final inFile = args.rest[0]; + final outFile = args['out'] as String?; + if (outFile == null) { + print('No output file.'); + exitCode = 1; + return; + } + + final inBytes = File(inFile).readAsBytesSync(); + final mod = WasmModule(inBytes); + print('$inFile compiled successfully'); + + print('\n=== Wasm module info ==='); + print(mod.describe()); + + final outBytes = mod.serialize(); + File(outFile).writeAsBytesSync(outBytes); + print('$outFile written successfully'); + return; +} diff --git a/wasm/lib/src/module.dart b/wasm/lib/src/module.dart index 2fc2677..9c1862b 100644 --- a/wasm/lib/src/module.dart +++ b/wasm/lib/src/module.dart @@ -16,9 +16,10 @@ class WasmModule { late final Pointer _module; /// Compile a module. - WasmModule(Uint8List data) { - _module = runtime.compile(this, data); - } + WasmModule(Uint8List data) : this._(data, false); + + /// Deserialize a module. See [WasmModule.serialize] for more details. + WasmModule.deserialize(Uint8List data) : this._(data, true); /// Returns a [WasmInstanceBuilder] that is used to add all the imports that /// the module needs before instantiating it. @@ -29,6 +30,13 @@ class WasmModule { WasmMemory createMemory(int pages, [int? maxPages]) => WasmMemory._create(pages, maxPages); + /// Serialize the module to a [Uint8List]. + /// + /// This buffer can be saved to a file, sent to another device, even with a + /// different CPU architecture, and then passed to [WasmModule.deserialize]. + /// Doing so speeds up start up time, by skipping compilation. + Uint8List serialize() => runtime.serialize(_module); + /// Returns a description of all of the module's imports and exports, for /// debugging. String describe() { @@ -43,6 +51,10 @@ class WasmModule { } return description.toString(); } + + WasmModule._(Uint8List data, bool isSerialized) { + _module = runtime.loadModule(this, data, isSerialized); + } } Pointer _wasmFnImportTrampoline( diff --git a/wasm/lib/src/runtime.g.dart b/wasm/lib/src/runtime.g.dart index c4771bc..731415c 100644 --- a/wasm/lib/src/runtime.g.dart +++ b/wasm/lib/src/runtime.g.dart @@ -99,9 +99,11 @@ class WasmRuntime { late final WasmerMemorytypeDeleteFn _memorytype_delete; late final WasmerMemorytypeNewFn _memorytype_new; late final WasmerModuleDeleteFn _module_delete; + late final WasmerModuleDeserializeFn _module_deserialize; late final WasmerModuleExportsFn _module_exports; late final WasmerModuleImportsFn _module_imports; late final WasmerModuleNewFn _module_new; + late final WasmerModuleSerializeFn _module_serialize; late final WasmerStoreDeleteFn _store_delete; late final WasmerStoreNewFn _store_new; late final WasmerTrapDeleteFn _trap_delete; @@ -436,6 +438,10 @@ class WasmRuntime { _lib.lookupFunction( 'wasm_module_delete', ); + _module_deserialize = _lib.lookupFunction( + 'wasm_module_deserialize', + ); _module_exports = _lib.lookupFunction( 'wasm_module_exports', @@ -448,6 +454,10 @@ class WasmRuntime { _lib.lookupFunction( 'wasm_module_new', ); + _module_serialize = _lib + .lookupFunction( + 'wasm_module_serialize', + ); _store_delete = _lib.lookupFunction( 'wasm_store_delete', @@ -511,9 +521,10 @@ class WasmRuntime { _set_finalizer_for_store(this, _store); } - Pointer compile( + Pointer loadModule( Object owner, Uint8List data, + bool isSerialized, ) { var dataPtr = calloc(data.length); for (var i = 0; i < data.length; ++i) { @@ -523,16 +534,33 @@ class WasmRuntime { dataVec.ref.data = dataPtr; dataVec.ref.length = data.length; - var modulePtr = _module_new(_store, dataVec); + var modulePtr = isSerialized + ? _module_deserialize(_store, dataVec) + : _module_new(_store, dataVec); calloc.free(dataPtr); calloc.free(dataVec); - _checkNotEqual(modulePtr, nullptr, 'Wasm module compile failed.'); + _checkNotEqual( + modulePtr, + nullptr, + 'Wasm module ${isSerialized ? 'deserialization' : 'compilation'} failed.', + ); _set_finalizer_for_module(owner, modulePtr); return modulePtr; } + Uint8List serialize(Pointer module) { + final outVec = calloc(); + outVec.ref.data = nullptr; + outVec.ref.length = 0; + _module_serialize(module, outVec); + final result = Uint8List.fromList(outVec.ref.list); + _byte_vec_delete(outVec); + calloc.free(outVec); + return result; + } + Pointer _externTypeToFuncOrGlobalType( int kind, Pointer extern, diff --git a/wasm/lib/src/wasmer_api.g.dart b/wasm/lib/src/wasmer_api.g.dart index 17cb8bf..8d2e174 100644 --- a/wasm/lib/src/wasmer_api.g.dart +++ b/wasm/lib/src/wasmer_api.g.dart @@ -571,6 +571,12 @@ typedef WasmerMemorytypeNewFn = Pointer Function( typedef NativeWasmerModuleDeleteFn = Void Function(Pointer); typedef WasmerModuleDeleteFn = void Function(Pointer); +// wasm_module_deserialize +typedef NativeWasmerModuleDeserializeFn = Pointer Function( + Pointer, Pointer); +typedef WasmerModuleDeserializeFn = Pointer Function( + Pointer, Pointer); + // wasm_module_exports typedef NativeWasmerModuleExportsFn = Void Function( Pointer, Pointer); @@ -589,6 +595,12 @@ typedef NativeWasmerModuleNewFn = Pointer Function( typedef WasmerModuleNewFn = Pointer Function( Pointer, Pointer); +// wasm_module_serialize +typedef NativeWasmerModuleSerializeFn = Void Function( + Pointer, Pointer); +typedef WasmerModuleSerializeFn = void Function( + Pointer, Pointer); + // wasm_store_delete typedef NativeWasmerStoreDeleteFn = Void Function(Pointer); typedef WasmerStoreDeleteFn = void Function(Pointer); diff --git a/wasm/test/corrupted_error_test.dart b/wasm/test/corrupted_error_test.dart index 4ff5d08..90f024f 100644 --- a/wasm/test/corrupted_error_test.dart +++ b/wasm/test/corrupted_error_test.dart @@ -23,7 +23,7 @@ void main() { () => WasmModule(data), throwsWasmError( allOf( - contains('Wasm module compile failed.'), + contains('Wasm module compilation failed.'), contains('Validation error: Bad magic number (at offset 0)'), ), ), diff --git a/wasm/test/serialize_test.dart b/wasm/test/serialize_test.dart new file mode 100644 index 0000000..24ba84d --- /dev/null +++ b/wasm/test/serialize_test.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Test that we can serialize and deserialize a wasm module. +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:test/test.dart'; +import 'package:wasm/wasm.dart'; + +void main() { + test('serializing and deserializing module', () { + // int64_t square(int64_t n) { return n * n; } + final data = Uint8List.fromList([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x06, 0x01, 0x60, // + 0x01, 0x7e, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x04, 0x05, 0x01, 0x70, + 0x01, 0x01, 0x01, 0x05, 0x03, 0x01, 0x00, 0x02, 0x06, 0x08, 0x01, 0x7f, + 0x01, 0x41, 0x80, 0x88, 0x04, 0x0b, 0x07, 0x13, 0x02, 0x06, 0x6d, 0x65, + 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x06, 0x73, 0x71, 0x75, 0x61, 0x72, + 0x65, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x00, + 0x7e, 0x0b, + ]); + + final mod = WasmModule(data); + final serialized = mod.serialize(); + + final inst = WasmModule.deserialize(serialized).builder().build(); + final fn = inst.lookupFunction('square'); + final n = fn(1234) as int; + expect(n, 1234 * 1234); + }); + + test('deserializing module from file', () { + // int64_t square(int64_t n) { return n * n; } + final serialized = File('test/test_files/serialized').readAsBytesSync(); + final inst = WasmModule.deserialize(serialized).builder().build(); + final fn = inst.lookupFunction('square'); + final n = fn(1234) as int; + expect(n, 1234 * 1234); + }); +} diff --git a/wasm/test/test_files/serialized b/wasm/test/test_files/serialized new file mode 100644 index 0000000..5d6f016 Binary files /dev/null and b/wasm/test/test_files/serialized differ diff --git a/wasm/tool/generate_ffi_boilerplate.py b/wasm/tool/generate_ffi_boilerplate.py index e9a040c..4f55bff 100755 --- a/wasm/tool/generate_ffi_boilerplate.py +++ b/wasm/tool/generate_ffi_boilerplate.py @@ -339,6 +339,8 @@ def declareType(name, withCopy=True): WASM_API_EXTERN own wasm_trap_t* wasm_trap_new(wasm_store_t* store, const wasm_message_t*); WASM_API_EXTERN void wasm_trap_message(const wasm_trap_t*, own wasm_message_t* out); WASM_API_EXTERN wasm_valkind_t wasm_valtype_kind(const wasm_valtype_t*); +WASM_API_EXTERN void wasm_module_serialize(const wasm_module_t*, own wasm_byte_vec_t* out); +WASM_API_EXTERN own wasm_module_t* wasm_module_deserialize(wasm_store_t*, const wasm_byte_vec_t*); wasi_config_t* wasi_config_new(const uint8_t* program_name); wasi_env_t* wasi_env_new(wasi_config_t* config); diff --git a/wasm/tool/runtime_template.dart.t b/wasm/tool/runtime_template.dart.t index b8f8651..ef6e000 100644 --- a/wasm/tool/runtime_template.dart.t +++ b/wasm/tool/runtime_template.dart.t @@ -32,9 +32,10 @@ class WasmRuntime { _set_finalizer_for_store(this, _store); } - Pointer compile( + Pointer loadModule( Object owner, Uint8List data, + bool isSerialized, ) { var dataPtr = calloc(data.length); for (var i = 0; i < data.length; ++i) { @@ -44,16 +45,33 @@ class WasmRuntime { dataVec.ref.data = dataPtr; dataVec.ref.length = data.length; - var modulePtr = _module_new(_store, dataVec); + var modulePtr = isSerialized + ? _module_deserialize(_store, dataVec) + : _module_new(_store, dataVec); calloc.free(dataPtr); calloc.free(dataVec); - _checkNotEqual(modulePtr, nullptr, 'Wasm module compile failed.'); + _checkNotEqual( + modulePtr, + nullptr, + 'Wasm module ${isSerialized ? 'deserialization' : 'compilation'} failed.', + ); _set_finalizer_for_module(owner, modulePtr); return modulePtr; } + Uint8List serialize(Pointer module) { + final outVec = calloc(); + outVec.ref.data = nullptr; + outVec.ref.length = 0; + _module_serialize(module, outVec); + final result = Uint8List.fromList(outVec.ref.list); + _byte_vec_delete(outVec); + calloc.free(outVec); + return result; + } + Pointer _externTypeToFuncOrGlobalType( int kind, Pointer extern,