Bug 1500203 - Baldr: add wasmCompileInSeparateProcess() shell testing function and tests using it (r=bbouvier)
authorLuke Wagner <luke@mozilla.com>
Tue, 06 Nov 2018 13:09:04 -0600
changeset 501099 6e842238034c
parent 501098 f4068f0a3955
child 501177 b8c47ee52501
child 501206 f62458d789a5
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbbouvier
bugs1500203
milestone65.0a1
first release with
nightly linux32
6e842238034c / 65.0a1 / 20181106220104 / files
nightly linux64
6e842238034c / 65.0a1 / 20181106220104 / files
nightly mac
6e842238034c / 65.0a1 / 20181106220104 / files
nightly win32
6e842238034c / 65.0a1 / 20181106220104 / files
nightly win64
6e842238034c / 65.0a1 / 20181106220104 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1500203 - Baldr: add wasmCompileInSeparateProcess() shell testing function and tests using it (r=bbouvier)
js/src/jit-test/tests/wasm/bench/wasm_box2d.js
js/src/jit-test/tests/wasm/caching.js
js/src/shell/js.cpp
js/src/wasm/WasmCompile.cpp
js/src/wasm/WasmCompile.h
js/src/wasm/WasmGenerator.cpp
js/src/wasm/WasmGenerator.h
js/src/wasm/WasmJS.cpp
js/src/wasm/WasmJS.h
--- a/js/src/jit-test/tests/wasm/bench/wasm_box2d.js
+++ b/js/src/jit-test/tests/wasm/bench/wasm_box2d.js
@@ -4,18 +4,18 @@ const isSimulator = build['arm-simulator
                     build['mips32-simulator'] ||
                     build['mips64-simulator'];
 
 // This test often times out on debug simulators due to the extreme slowdown.
 if (build['debug'] && isSimulator)
   quit();
 
 // All the glue code is wrapped in a function so it can be executed uncached and
-// cached (via the shared cacheEntry argument).
-function runBox2d(cacheEntry) {
+// cached or compiled separately.
+function runBox2d(cacheEntryOrModule) {
 
 // The Module object: Our interface to the outside world. We import
 // and export values on it, and do the work to get that through
 // closure compiler if necessary. There are various ways Module can be used:
 // 1. Not defined. We create it here
 // 2. A function parameter, function(Module) { ..generated code.. }
 // 3. pre-run appended it, var Module = {}; ..generated code..
 // 4. External script tag defines var Module.
@@ -1494,22 +1494,25 @@ function integrateWasmJS(Module) {
       exports = instance.exports;
       if (exports.memory) mergeMemory(exports.memory);
       Module['asm'] = exports;
       Module["usingWasm"] = true;
     }
     Module['printErr']('asynchronously preparing wasm');
     addRunDependency('wasm-instantiate'); // we can't run yet
 
-    (wasmStreamingIsSupported()
-     ? WebAssembly.instantiateStreaming(cacheEntry, info)
-     : WebAssembly.instantiate(cacheEntry.getBuffer(), info))
+    (cacheEntryOrModule instanceof WebAssembly.Module
+     ? WebAssembly.instantiate(cacheEntryOrModule, info)
+       .then(instance => ({instance, module:cacheEntryOrModule}))
+     : wasmStreamingIsSupported()
+       ? WebAssembly.instantiateStreaming(cacheEntryOrModule, info)
+       : WebAssembly.instantiate(cacheEntryOrModule.getBuffer(), info))
     .then(function(output) {
-      if (!cacheEntry.module)
-        cacheEntry.module = output.module;
+      if (!cacheEntryOrModule.module)
+        cacheEntryOrModule.module = output.module;
 
       // receiveInstance() will swap in the exports (to Module.asm) so they can be called
       receiveInstance(output.instance);
       removeRunDependency('wasm-instantiate');
     })
     .catch(function(reason) {
       Module['printErr']('failed to asynchronously prepare wasm:\n  ' + reason);
     });
@@ -3039,17 +3042,23 @@ if (Module['noInitialRun']) {
 }
 
 
 run();
 drainJobQueue();
 
 };  // End of function wrapping the whole top-level Emscripten glue code
 
+const bytecode = os.file.readFile(scriptdir + 'wasm_box2d.wasm', 'binary');
+
 setBufferStreamParams(/* delayMillis = */ 1, /* chunkSize = */ 1000);
-const cacheEntry = streamCacheEntry(os.file.readFile(scriptdir + 'wasm_box2d.wasm', 'binary'));
+const cacheEntry = streamCacheEntry(bytecode);
 
 runBox2d(cacheEntry);
 
 while (!wasmHasTier2CompilationCompleted(cacheEntry.module)) sleep(1);
 assertEq(cacheEntry.cached, wasmCachingIsSupported());
 
 runBox2d(cacheEntry);
+
+if (wasmCachingIsSupported()) {
+    runBox2d(wasmCompileInSeparateProcess(bytecode));
+}
--- a/js/src/jit-test/tests/wasm/caching.js
+++ b/js/src/jit-test/tests/wasm/caching.js
@@ -16,24 +16,46 @@ function testCached(code, imports, test)
             sleep(1);
          }
          assertEq(cache.cached, wasmCachingIsSupported());
          return compileStreaming(cache);
      })
      .then(m => {
          test(new Instance(m, imports));
          assertEq(cache.cached, wasmCachingIsSupported());
+
+         if (wasmCachingIsSupported()) {
+             let m2 = wasmCompileInSeparateProcess(code);
+             test(new Instance(m2, imports));
+         }
+
          success = true;
      })
      .catch(err => { print(String(err) + " at:\n" + err.stack) });
 
      drainJobQueue();
      assertEq(success, true);
 }
 
+testCached(`(module
+    (func $test (param i64) (result f64)
+        get_local 0
+        f64.convert_u/i64
+    )
+    (func (export "run") (result i32)
+        i64.const 1
+        call $test
+        f64.const 1
+        f64.eq
+    )
+)`,
+    undefined,
+    i => { assertEq(i.exports.run(), 1); }
+);
+
 testCached(
     `(module
        (func (export "run") (result i32)
          (i32.const 42)))`,
     undefined,
     i => { assertEq(i.exports.run(), 42); }
 );
 
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -5629,39 +5629,16 @@ runOffThreadDecodedScript(JSContext* cx,
     RootedScript script(cx, JS::FinishOffThreadScriptDecoder(cx, token));
     if (!script) {
         return false;
     }
 
     return JS_ExecuteScript(cx, script, args.rval());
 }
 
-struct MOZ_RAII FreeOnReturn
-{
-    JSContext* cx;
-    const char* ptr;
-    MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
-
-    explicit FreeOnReturn(JSContext* cx, const char* ptr = nullptr
-                 MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
-      : cx(cx), ptr(ptr)
-    {
-        MOZ_GUARD_OBJECT_NOTIFIER_INIT;
-    }
-
-    void init(const char* ptr) {
-        MOZ_ASSERT(!this->ptr);
-        this->ptr = ptr;
-    }
-
-    ~FreeOnReturn() {
-        JS_free(cx, (void*)ptr);
-    }
-};
-
 static int sArgc;
 static char** sArgv;
 
 class AutoCStringVector
 {
     Vector<char*> argv_;
   public:
     explicit AutoCStringVector(JSContext* cx) : argv_(cx) {}
@@ -5842,16 +5819,323 @@ NestedShell(JSContext* cx, unsigned argc
         return false;
     }
 
     args.rval().setUndefined();
     return true;
 }
 
 static bool
+ReadAll(int fd, wasm::Bytes* bytes)
+{
+    size_t lastLength = bytes->length();
+    while (true) {
+        static const int ChunkSize = 64 * 1024;
+        if (!bytes->growBy(ChunkSize)) {
+            return false;
+        }
+
+        intptr_t readCount;
+        while (true) {
+            readCount = read(fd, bytes->begin() + lastLength, ChunkSize);
+            if (readCount >= 0) {
+                break;
+            }
+            if (errno != EINTR) {
+                return false;
+            }
+        }
+
+        if (readCount < ChunkSize) {
+            bytes->shrinkTo(lastLength + readCount);
+            if (readCount == 0) {
+                return true;
+            }
+        }
+
+        lastLength = bytes->length();
+    }
+}
+
+static bool
+WriteAll(int fd, const uint8_t* bytes, size_t length)
+{
+    while (length > 0) {
+        int written = write(fd, bytes, length);
+        if (written < 0) {
+            if (errno == EINTR) {
+                continue;
+            }
+            return false;
+        }
+        MOZ_ASSERT(unsigned(written) <= length);
+        length -= written;
+        bytes += written;
+    }
+
+    return true;
+}
+
+class AutoPipe
+{
+    int fds_[2];
+
+  public:
+    AutoPipe()
+    {
+        fds_[0] = -1;
+        fds_[1] = -1;
+    }
+
+    ~AutoPipe() {
+        if (fds_[0] != -1) {
+            close(fds_[0]);
+        }
+        if (fds_[1] != -1) {
+            close(fds_[1]);
+        }
+    }
+
+    bool init() {
+#ifdef XP_WIN
+        return !_pipe(fds_, 4096, O_BINARY);
+#else
+        return !pipe(fds_);
+#endif
+    }
+
+    int reader() const {
+        MOZ_ASSERT(fds_[0] != -1);
+        return fds_[0];
+    }
+
+    int writer() const {
+        MOZ_ASSERT(fds_[1] != -1);
+        return fds_[1];
+    }
+
+    void closeReader() {
+        MOZ_ASSERT(fds_[0] != -1);
+        close(fds_[0]);
+        fds_[0] = -1;
+    }
+
+    void closeWriter() {
+        MOZ_ASSERT(fds_[1] != -1);
+        close(fds_[1]);
+        fds_[1] = -1;
+    }
+};
+
+static bool
+CompileAndSerializeInSeparateProcess(JSContext* cx, const uint8_t* bytecode, size_t bytecodeLength,
+                                     wasm::Bytes* serialized)
+{
+    AutoPipe stdIn, stdOut;
+    if (!stdIn.init() || !stdOut.init()) {
+        return false;
+    }
+
+    AutoCStringVector argv(cx);
+
+    UniqueChars argv0 = DuplicateString(cx, sArgv[0]);
+    if (!argv0 || !argv.append(std::move(argv0))) {
+        return false;
+    }
+
+    UniqueChars argv1 = DuplicateString("--wasm-compile-and-serialize");
+    if (!argv1 || !argv.append(std::move(argv1))) {
+        return false;
+    }
+
+#ifdef XP_WIN
+    // The spawned process will have all the stdIn/stdOut file handles open, but
+    // without the power of fork, we need some other way to communicate the
+    // integer fd values so we encode them in argv and WasmCompileAndSerialize()
+    // has a matching #ifdef XP_WIN to parse them out. Communicate both ends of
+    // both pipes so the child process can closed the unused ends.
+
+    UniqueChars argv2 = JS_smprintf("%d", stdIn.reader());
+    if (!argv2 || !argv.append(std::move(argv2))) {
+        return false;
+    }
+
+    UniqueChars argv3 = JS_smprintf("%d", stdIn.writer());
+    if (!argv3 || !argv.append(std::move(argv3))) {
+        return false;
+    }
+
+    UniqueChars argv4 = JS_smprintf("%d", stdOut.reader());
+    if (!argv4 || !argv.append(std::move(argv4))) {
+        return false;
+    }
+
+    UniqueChars argv5 = JS_smprintf("%d", stdOut.writer());
+    if (!argv5 || !argv.append(std::move(argv5))) {
+        return false;
+    }
+#endif
+
+    for (unsigned i = 0; i < sPropagatedFlags.length(); i++) {
+        UniqueChars flags = DuplicateString(cx, sPropagatedFlags[i]);
+        if (!flags || !argv.append(std::move(flags))) {
+            return false;
+        }
+    }
+
+    if (!argv.append(nullptr)) {
+        return false;
+    }
+
+#ifdef XP_WIN
+    if (!EscapeForShell(cx, argv)) {
+        return false;
+    }
+
+    int childPid = _spawnv(P_NOWAIT, sArgv[0], argv.get());
+    if (childPid == -1) {
+        return false;
+    }
+#else
+    pid_t childPid = fork();
+    switch (childPid) {
+      case -1:
+        return false;
+      case 0:
+        // In the child process. Redirect stdin/stdout to the respective ends of
+        // the pipes. Closing stdIn.writer() is necessary for stdin to hit EOF.
+        // This case statement must not return before exec() takes over. Rather,
+        // exit(-1) is used to return failure to the parent process.
+        if (dup2(stdIn.reader(), STDIN_FILENO) == -1) {
+            exit(-1);
+        }
+        if (dup2(stdOut.writer(), STDOUT_FILENO) == -1) {
+            exit(-1);
+        }
+        close(stdIn.reader());
+        close(stdIn.writer());
+        close(stdOut.reader());
+        close(stdOut.writer());
+        execv(sArgv[0], argv.get());
+        exit(-1);
+    }
+#endif
+
+    // In the parent process. Closing stdOut.writer() is necessary for
+    // stdOut.reader() below to hit EOF.
+    stdIn.closeReader();
+    stdOut.closeWriter();
+
+    if (!WriteAll(stdIn.writer(), bytecode, bytecodeLength)) {
+        return false;
+    }
+
+    stdIn.closeWriter();
+
+    if (!ReadAll(stdOut.reader(), serialized)) {
+        return false;
+    }
+
+    stdOut.closeReader();
+
+    int status;
+#ifdef XP_WIN
+    if (_cwait(&status, childPid, WAIT_CHILD) == -1) {
+        return false;
+    }
+#else
+    while (true) {
+        if (waitpid(childPid, &status, 0) >= 0) {
+            break;
+        }
+        if (errno != EINTR) {
+            return false;
+        }
+    }
+#endif
+
+    return status == 0;
+}
+
+static bool
+WasmCompileAndSerialize(JSContext* cx)
+{
+    MOZ_ASSERT(wasm::HasCachingSupport(cx));
+
+#ifdef XP_WIN
+    // See CompileAndSerializeInSeparateProcess for why we've had to smuggle
+    // these fd values through argv. Closing the writing ends is necessary for
+    // the reading ends to hit EOF.
+    MOZ_RELEASE_ASSERT(sArgc >= 6);
+    MOZ_ASSERT(!strcmp(sArgv[1], "--wasm-compile-and-serialize"));
+    int stdIn = atoi(sArgv[2]);   // stdIn.reader()
+    close(atoi(sArgv[3]));        // stdIn.writer()
+    close(atoi(sArgv[4]));        // stdOut.reader()
+    int stdOut = atoi(sArgv[5]);  // stdOut.writer()
+#else
+    int stdIn = STDIN_FILENO;
+    int stdOut = STDOUT_FILENO;
+#endif
+
+    wasm::MutableBytes bytecode = js_new<wasm::ShareableBytes>();
+    if (!ReadAll(stdIn, &bytecode->bytes)) {
+        return false;
+    }
+
+    wasm::Bytes serialized;
+    if (!wasm::CompileAndSerialize(*bytecode, &serialized)) {
+        return false;
+    }
+
+    if (!WriteAll(stdOut, serialized.begin(), serialized.length())) {
+        return false;
+    }
+
+    return true;
+}
+
+static bool
+WasmCompileInSeparateProcess(JSContext* cx, unsigned argc, Value* vp)
+{
+    if (!wasm::HasCachingSupport(cx)) {
+        JS_ReportErrorASCII(cx, "WebAssembly caching not supported");
+        return false;
+    }
+
+    CallArgs args = CallArgsFromVp(argc, vp);
+    if (!args.requireAtLeast(cx, "wasmCompileInSeparateProcess", 1)) {
+        return false;
+    }
+
+    SharedMem<uint8_t*> bytecode;
+    size_t numBytes;
+    if (!args[0].isObject() || !IsBufferSource(&args[0].toObject(), &bytecode, &numBytes)) {
+        RootedObject callee(cx, &args.callee());
+        ReportUsageErrorASCII(cx, callee, "Argument must be a buffer source");
+        return false;
+    }
+
+    wasm::Bytes serialized;
+    if (!CompileAndSerializeInSeparateProcess(cx, bytecode.unwrap(), numBytes, &serialized)) {
+        if (!cx->isExceptionPending()) {
+            JS_ReportErrorASCII(cx, "creating and executing child process");
+        }
+        return false;
+    }
+
+    RootedObject module(cx);
+    if (!wasm::DeserializeModule(cx, serialized, &module)) {
+        return false;
+    }
+
+    args.rval().setObject(*module);
+    return true;
+}
+
+static bool
 DecompileFunction(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     if (args.length() < 1 || !args[0].isObject() || !args[0].toObject().is<JSFunction>()) {
         args.rval().setUndefined();
         return true;
     }
     RootedFunction fun(cx, &args[0].toObject().as<JSFunction>());
@@ -8607,16 +8891,22 @@ JS_FN_HELP("parseBin", BinParse, 1, 0,
 "addIntlExtras(obj)",
 "Adds various not-yet-standardized Intl functions as properties on the\n"
 "provided object (this should generally be Intl itself).  The added\n"
 "functions and their behavior are experimental: don't depend upon them\n"
 "unless you're willing to update your code if these experimental APIs change\n"
 "underneath you."),
 #endif // ENABLE_INTL_API
 
+    JS_FN_HELP("wasmCompileInSeparateProcess", WasmCompileInSeparateProcess, 1, 0,
+"wasmCompileInSeparateProcess(buffer)",
+"  Compile the given buffer in a separate process, serialize the resulting\n"
+"  wasm::Module into bytes, and deserialize those bytes in the current\n"
+"  process, returning the resulting WebAssembly.Module."),
+
     JS_FS_HELP_END
 };
 
 static const JSFunctionSpecWithHelp fuzzing_unsafe_functions[] = {
     JS_FN_HELP("getSelfHostedValue", GetSelfHostedValue, 1, 0,
 "getSelfHostedValue()",
 "  Get a self-hosted value by its name. Note that these values don't get \n"
 "  cached, so repeatedly getting the same value creates multiple distinct clones."),
@@ -10405,16 +10695,25 @@ SetWorkerContextOptions(JSContext* cx)
 #endif
 
     JS_SetNativeStackQuota(cx, gMaxStackSize);
 }
 
 static int
 Shell(JSContext* cx, OptionParser* op, char** envp)
 {
+    if (op->getBoolOption("wasm-compile-and-serialize")) {
+        if (!WasmCompileAndSerialize(cx)) {
+            // Errors have been printed directly to stderr.
+            MOZ_ASSERT(!cx->isExceptionPending());
+            return -1;
+        }
+        return EXIT_SUCCESS;
+    }
+
 #ifdef MOZ_CODE_COVERAGE
     InstallCoverageSignalHandlers();
 #endif
 
     Maybe<JS::AutoDisableGenerationalGC> noggc;
     if (op->getBoolOption("no-ggc")) {
         noggc.emplace(cx);
     }
@@ -10780,16 +11079,17 @@ main(int argc, char** argv, char** envp)
         || !op.addStringOption('z', "gc-zeal", "LEVEL(;LEVEL)*[,N]", gc::ZealModeHelpText)
 #else
         || !op.addStringOption('z', "gc-zeal", "LEVEL(;LEVEL)*[,N]", "option ignored in non-gc-zeal builds")
 #endif
         || !op.addStringOption('\0', "module-load-path", "DIR", "Set directory to load modules from")
         || !op.addBoolOption('\0', "no-async-stacks", "Disable async stacks")
         || !op.addMultiStringOption('\0', "dll", "LIBRARY", "Dynamically load LIBRARY")
         || !op.addBoolOption('\0', "suppress-minidump", "Suppress crash minidumps")
+        || !op.addBoolOption('\0', "wasm-compile-and-serialize", "Compile the wasm bytecode from stdin and serialize the results to stdout")
     )
     {
         return EXIT_FAILURE;
     }
 
     op.setArgTerminatesOptions("script", true);
     op.setArgCapturesRest("scriptArgs");
 
--- a/js/src/wasm/WasmCompile.cpp
+++ b/js/src/wasm/WasmCompile.cpp
@@ -525,17 +525,17 @@ DecodeCodeSection(const ModuleEnvironmen
         return false;
     }
 
     return mg.finishFuncDefs();
 }
 
 SharedModule
 wasm::CompileBuffer(const CompileArgs& args, const ShareableBytes& bytecode, UniqueChars* error,
-                    UniqueCharsVector* warnings)
+                    UniqueCharsVector* warnings, UniqueLinkData* maybeLinkData)
 {
     MOZ_RELEASE_ASSERT(wasm::HaveSignalHandlers());
 
     Decoder d(bytecode.bytes, 0, error, warnings);
 
     CompilerEnvironment compilerEnv(args);
     ModuleEnvironment env(args.gcTypesConfigured,
                           &compilerEnv,
@@ -564,17 +564,17 @@ wasm::CompileBuffer(const CompileArgs& a
     if (!DecodeCodeSection(env, d, mg)) {
         return nullptr;
     }
 
     if (!DecodeModuleTail(d, &env, mg.deferredValidationState())) {
         return nullptr;
     }
 
-    return mg.finishModule(bytecode);
+    return mg.finishModule(bytecode, nullptr, maybeLinkData);
 }
 
 void
 wasm::CompileTier2(const CompileArgs& args, const Bytes& bytecode, const Module& module,
                    Atomic<bool>* cancelled)
 {
     MOZ_RELEASE_ASSERT(wasm::HaveSignalHandlers());
 
--- a/js/src/wasm/WasmCompile.h
+++ b/js/src/wasm/WasmCompile.h
@@ -84,17 +84,18 @@ EstimateCompiledCodeSize(Tier tier, size
 // SharedModule pointer is null and either:
 //  - *error points to a string description of the error
 //  - *error is null and the caller should report out-of-memory.
 
 SharedModule
 CompileBuffer(const CompileArgs& args,
               const ShareableBytes& bytecode,
               UniqueChars* error,
-              UniqueCharsVector* warnings);
+              UniqueCharsVector* warnings,
+              UniqueLinkData* maybeLinkData = nullptr);
 
 // Attempt to compile the second tier of the given wasm::Module.
 
 void
 CompileTier2(const CompileArgs& args, const Bytes& bytecode, const Module& module,
              Atomic<bool>* cancelled);
 
 // Compile the given WebAssembly module which has been broken into three
--- a/js/src/wasm/WasmGenerator.cpp
+++ b/js/src/wasm/WasmGenerator.cpp
@@ -1032,17 +1032,17 @@ ModuleGenerator::finishMetadata(const By
     SharedMetadata metadata = metadata_;
     metadata_ = nullptr;
     return metadata;
 }
 
 SharedModule
 ModuleGenerator::finishModule(const ShareableBytes& bytecode,
                               JS::OptimizedEncodingListener* maybeTier2Listener,
-                              UniqueLinkData* maybeLinkDataOut)
+                              UniqueLinkData* maybeLinkData)
 {
     MOZ_ASSERT(mode() == CompileMode::Once || mode() == CompileMode::Tier1);
 
     UniqueCodeTier codeTier = finishCodeTier();
     if (!codeTier) {
         return nullptr;
     }
 
@@ -1149,20 +1149,19 @@ ModuleGenerator::finishModule(const Shar
     }
 
     if (mode() == CompileMode::Tier1) {
         module->startTier2(*compileArgs_, bytecode, maybeTier2Listener);
     } else if (tier() == Tier::Serialized && maybeTier2Listener) {
         module->serialize(*linkData_, *maybeTier2Listener);
     }
 
-    if (maybeLinkDataOut) {
-        MOZ_ASSERT(isAsmJS());
+    if (maybeLinkData) {
         MOZ_ASSERT(!env_->debugEnabled());
-        *maybeLinkDataOut = std::move(linkData_);
+        *maybeLinkData = std::move(linkData_);
     }
 
     return module;
 }
 
 bool
 ModuleGenerator::finishTier2(const Module& module)
 {
--- a/js/src/wasm/WasmGenerator.h
+++ b/js/src/wasm/WasmGenerator.h
@@ -228,17 +228,17 @@ class MOZ_STACK_CLASS ModuleGenerator
     MOZ_MUST_USE bool finishFuncDefs();
 
     // If env->mode is Once or Tier1, finishModule() must be called to generate
     // a new Module. Otherwise, if env->mode is Tier2, finishTier2() must be
     // called to augment the given Module with tier 2 code.
 
     SharedModule finishModule(const ShareableBytes& bytecode,
                               JS::OptimizedEncodingListener* maybeTier2Listener = nullptr,
-                              UniqueLinkData* maybeLinkDataOut = nullptr);
+                              UniqueLinkData* maybeLinkData = nullptr);
     MOZ_MUST_USE bool finishTier2(const Module& module);
 
     ExclusiveDeferredValidationState& deferredValidationState() {
         return deferredValidationState_;
     }
 };
 
 } // namespace wasm
--- a/js/src/wasm/WasmJS.cpp
+++ b/js/src/wasm/WasmJS.cpp
@@ -380,17 +380,17 @@ DescribeScriptedCaller(JSContext* cx, Sc
             return false;
         }
     }
 
     return true;
 }
 
 // ============================================================================
-// Fuzzing support
+// Testing / Fuzzing support
 
 bool
 wasm::Eval(JSContext* cx, Handle<TypedArrayObject*> code, HandleObject importObj,
            MutableHandleWasmInstanceObject instanceObj)
 {
     if (!GlobalObject::ensureConstructor(cx, cx->global(), JSProto_WebAssembly)) {
         return false;
     }
@@ -437,16 +437,60 @@ wasm::Eval(JSContext* cx, Handle<TypedAr
     if (!GetImports(cx, *module, importObj, &funcs, &table, &memory, globalObjs.get(), &globals)) {
         return false;
     }
 
     return module->instantiate(cx, funcs, table, memory, globals, globalObjs.get(), nullptr,
                                instanceObj);
 }
 
+bool
+wasm::CompileAndSerialize(const ShareableBytes& bytecode, Bytes* serialized)
+{
+    MutableCompileArgs compileArgs = js_new<CompileArgs>(ScriptedCaller());
+    if (!compileArgs) {
+        return false;
+    }
+
+    // The caller has ensured HasCachingSupport().
+    compileArgs->ionEnabled = true;
+
+    UniqueChars error;
+    UniqueCharsVector warnings;
+    UniqueLinkData linkData;
+    SharedModule module = CompileBuffer(*compileArgs, bytecode, &error, &warnings, &linkData);
+    if (!module) {
+        fprintf(stderr, "Compilation error: %s\n", error ? error.get() : "oom");
+        return false;
+    }
+
+    MOZ_ASSERT(module->code().hasTier(Tier::Serialized));
+
+    size_t serializedSize = module->serializedSize(*linkData);
+    if (!serialized->resize(serializedSize)) {
+        return false;
+    }
+
+    module->serialize(*linkData, serialized->begin(), serialized->length());
+    return true;
+}
+
+bool
+wasm::DeserializeModule(JSContext* cx, const Bytes& serialized, MutableHandleObject moduleObj)
+{
+    MutableModule module = Module::deserialize(serialized.begin(), serialized.length());
+    if (!module) {
+        ReportOutOfMemory(cx);
+        return false;
+    }
+
+    moduleObj.set(module->createObject(cx));
+    return !!moduleObj;
+}
+
 // ============================================================================
 // Common functions
 
 // '[EnforceRange] unsigned long' types are coerced with
 //    ConvertToInt(v, 32, 'unsigned')
 // defined in Web IDL Section 3.2.4.9.
 static bool
 EnforceRangeU32(JSContext* cx, HandleValue v, const char* kind, const char* noun, uint32_t* u32)
--- a/js/src/wasm/WasmJS.h
+++ b/js/src/wasm/WasmJS.h
@@ -59,16 +59,28 @@ HasCachingSupport(JSContext* cx);
 
 // Compiles the given binary wasm module given the ArrayBufferObject
 // and links the module's imports with the given import object.
 
 MOZ_MUST_USE bool
 Eval(JSContext* cx, Handle<TypedArrayObject*> code, HandleObject importObj,
      MutableHandleWasmInstanceObject instanceObj);
 
+// For testing cross-process (de)serialization, this pair of functions are
+// responsible for, in the child process, compiling the given wasm bytecode
+// to a wasm::Module that is serialized into the given byte array, and, in
+// the parent process, deserializing the given byte array into a
+// WebAssembly.Module object.
+
+MOZ_MUST_USE bool
+CompileAndSerialize(const ShareableBytes& bytecode, Bytes* serialized);
+
+MOZ_MUST_USE bool
+DeserializeModule(JSContext* cx, const Bytes& serialized, MutableHandleObject module);
+
 // These accessors can be used to probe JS values for being an exported wasm
 // function.
 
 extern bool
 IsExportedFunction(JSFunction* fun);
 
 extern bool
 IsExportedWasmFunction(JSFunction* fun);