Search code examples
javascriptc++node.jswebassembly

How can I capture stdout and stderr from a WebAssembly module generated by Emscripten in JavaScript?


I have the following C++ code

#include <iostream>

int main()
{
  std::cout << "Hello World!" << std::endl;
  return 0;
}

Which I compile with

emcc -s ENVIRONMENT=shell -s WASM=1 -s MODULARIZE=1 main.cpp -o main.js

The command above generates a main.js and a main.wasm.

Then, using isolated-vm

npm install isolated-vm

I have the following code in JavaScript

const fs = require("fs");
const ivm = require("isolated-vm");
const isolate = new ivm.Isolate({ memoryLimit: 128 });

const context = isolate.createContextSync();
const jail = context.global;

jail.setSync("global", jail.derefInto());

const hostile = isolate.compileScriptSync(fs.readFileSync("main.js", "utf8"));

hostile.run(context).catch((err) => console.error(err));

If I run

node index.js

It runs without any erros, but no stdout/stderr.

My question is: Using isolated-vm, how can I capture stdout and stderr to a JavaScript variable?

I searched for code on GitHub, but what I found didn't work.


EDIT

If I compile with these flags

emcc -s ENVIRONMENT=node -s WASM=1 main.cpp -o main.js && node main.js

It shows "Hello World!" in the terminal, however, if I try to run it as showed in the index.js above, I get the error

Error: not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)

Solution

  • I can get you half-way there. I would consider this a bug in isolated-vm, because if you pass vanilla javascript, it works as well as in the node version.

    $ em++ main.cpp -o main.js -s WASM=0 -s ENVIRONMENT=node && node main.js
    Hello World!
    $ em++ main.cpp -o main.js -s WASM=1 -s ENVIRONMENT=node && node main.js
    Hello World!
    $ em++ main.cpp -o main.js -s WASM=0 -s ENVIRONMENT=shell && node index.js
    Hello World!
    $ em++ main.cpp -o main.js -s WASM=1 -s ENVIRONMENT=shell && node index.js
    $
    

    So, the only case where this is not working, is when main.js uses wasm internally.

    Furthermore, when you actually run it in the v8 shell, it works as expected:

    $ em++ main.cpp -o main.js -s WASM=1 -s ENVIRONMENT=shell && v8 main.js
    Hello World!
    

    In order to get to this point, I had to modify index.js a bit:

    const fs = require("fs");
    const ivm = require("isolated-vm");
    const isolate = new ivm.Isolate({ memoryLimit: 128 });
    
    const context = isolate.createContextSync();
    const jail = context.global;
    
    jail.setSync("global", jail.derefInto());
    jail.setSync("print", function (...args) {
      console.log(...args);
    });
    
    readbuffer = function(f) {
     return fs.readFileSync(f);
    };
    
    jail.setSync("readbuffer", readbuffer);
    
    const hostile = isolate.compileScriptSync(fs.readFileSync("main.js", "utf8"));
    
    hostile.run(context).catch((err) => console.error(err));
    

    Emscripten-compiled modules prefer to use the print function before the console.log function. Also, for some reason, the read function wasn't setup for main.js to use, and neither was readbuffer. You need one of them, in order to read main.wasm off the disk.

    As a nitpick, I know the original example was hostile, but you should change the name of the variable now, as this is benign :)

    I'll keep an eye on the question and try some more stuff, as well as hoping a more experienced answerer comes along, but for me, you should ask the maintainers of isolated-vm why it behaves like this.