swiftwasm / swift

WebAssembly support for the Swift programming language
https://swiftwasm.org
1.32k stars 28 forks source link

Error 71 with Node.js WASI/WASM #4754

Closed kennu closed 2 years ago

kennu commented 2 years ago

Describe the bug When I load and execute a simple helloworld.wasm file, built with SwiftWasm, in Node.js 16, it terminates with exit code 71 before outputting anything.

Steps To Reproduce Steps to reproduce the behavior:

  1. Build simple wasm that just prints hello world.
  2. Execute it in Node.js according to https://nodejs.org/api/wasi.html
  3. WASI.start() returns code 71 and there is no output. ...

Expected behavior Program should print hello world and exit with code 0.

Environment (please fill out the following information)

Additional context Many other wasm compilers work fine with the same setup (Clang, Rust, Go, Grain, Zig, AssemlyScript) so I'm trying to figure out what is different in SwiftWasm.

kateinoigakukun commented 2 years ago

Could you elaborate "Build simple wasm that just prints hello world." more to reproduce your problem on my side? Also please report the JS harness script for launching your wasi app.

kennu commented 2 years ago

The hello world .swift program that I tried to execute looks like this, using only print() functions:

It is built with docker run --rm -it -v $PWD:/app -w /app ghcr.io/swiftwasm/swift:5.6 swiftc -target wasm32-unknown-wasi demo-swift.swift -o demo-swift.wasm

demo-swift.swift:

print("{")
print("  \"statusCode\": \"200\",")
print("  \"headers\": {")
print("    \"Content-Type\":\"application/json\"")
print("  },")
print("  \"body\": \"{ \\\"message\\\": \\\"Hello world from Swift application!\\\" }\"")
print("}")

The Node.js WebAssembly loader code looks like this:

import { join } from 'path'
import { openSync, closeSync, readFileSync } from 'fs'
// @ts-ignore
import { WASI } from 'wasi'

async function runWasm(): Promise<Buffer> {
  const wasmPath = join(__dirname, 'main.wasm')
  const wasmContent = readFileSync(wasmPath)
  const wasmLength = wasmContent.length
  console.log('Executing WASM file at', wasmPath, 'with', wasmLength, 'bytes')

  // Capture output to a temporary file
  const tempFileName = `/tmp/output-${process.env.AWS_LAMBDA_LOG_STREAM_NAME?.replace(/[^a-z0-9]/g, '')}.txt`
  const tempFileFd = openSync(tempFileName, 'w+', 0o666)

  const wasi = new WASI({
    args: [],
    env: {}, // process.env,
    preopens: {
      // '/': '/',
    },
    returnOnExit: true,
    stdout: tempFileFd,
  })

  // @ts-ignore
  const wasm = await WebAssembly.compile(wasmContent)
  // @ts-ignore
  const instance = await WebAssembly.instantiate(wasm, {
    wasi_snapshot_preview1: wasi.wasiImport,
  })
  const exitCode = wasi.start(instance)
  console.log('WebAssembly exit code:', exitCode)
  closeSync(tempFileFd)
  const output = readFileSync(tempFileName)
  console.log('WebAssembly output (', output?.length, '):', output?.toString())
  return output
}
kennu commented 2 years ago

Here is a simpler Node.js 16.x loader that works from command line as

node --experimental-wasi-unstable-preview1 loader.js ../wasm/swift/demo-swift.wasm

With the SwiftWasm-compiled wasm, I get exit code 71. With e.g. AssemblyScript compiled wasm, I get expected output and exit code 0.

loader.js:

const fs = require('fs')
const wasiMod = require('wasi')

async function main() {
  const wasmPath = process.argv[2]
  const wasmContent = fs.readFileSync(wasmPath)
  const wasmLength = wasmContent.length
  console.log('Executing WASM file at', wasmPath, 'with', wasmLength, 'bytes')

  const wasi = new wasiMod.WASI({
    args: [],
    env: {},
    preopens: {},
    returnOnExit: true,
  })

  const wasm = await WebAssembly.compile(wasmContent)
  const instance = await WebAssembly.instantiate(wasm, {
    wasi_snapshot_preview1: wasi.wasiImport,
  })
  const exitCode = wasi.start(instance)
  console.log('WebAssembly exit code:', exitCode)
}

main().then(console.log).catch(console.error)
kateinoigakukun commented 2 years ago

Thank you for your info. I could reproduce your issue.

The root cause of this issue is you didn't pass "program name" as args[0] and wasi-libc requires to have it. (so C code compiled with WASI SDK also results the same result)

So passing args: ["main"] to WASI constructor would solve your issue. I will note this on our documentation.

Thank you.

kennu commented 2 years ago

Awesome @kateinoigakukun thanks! I never thought of it. After adding args[0] it works fine.

Node.js docs/default are a little conflicting, saying "The first argument is the virtual path to the WASI command itself. Default: []". I guess many languages ignore argv[0] until you use it, but now I know to always include it.