diff --git a/src/shell/Shell.cpp b/src/shell/Shell.cpp index f5ba4056d..59f5b833f 100644 --- a/src/shell/Shell.cpp +++ b/src/shell/Shell.cpp @@ -43,6 +43,7 @@ struct ParseOptions { // WASI options std::vector wasi_envs; std::vector> wasi_dirs; + int argsIndex = -1; }; static uint32_t s_JITFlags = 0; @@ -1039,7 +1040,7 @@ static void runExports(Store* store, const std::string& filename, const std::vec &data); } -static void parseArguments(int argc, char* argv[], ParseOptions& options) +static void parseArguments(const int argc, const char* argv[], ParseOptions& options) { for (int i = 1; i < argc; i++) { if (strlen(argv[i]) >= 2 && argv[i][0] == '-') { // parse command line option @@ -1083,6 +1084,15 @@ static void parseArguments(int argc, char* argv[], ParseOptions& options) options.wasi_dirs.push_back(std::make_pair(argv[i + 2], argv[i + 1])); i += 2; continue; + } else if (strcmp(argv[i], "--args") == 0) { + if (i + 1 == argc || argv[i + 1][0] == '-') { + fprintf(stderr, "error: --args requires an argument\n"); + exit(1); + } + ++i; + options.fileNames.emplace_back(argv[i]); + options.argsIndex = i; + break; } else if (strcmp(argv[i], "--help") == 0) { fprintf(stdout, "Usage: walrus [OPTIONS] \n\n"); fprintf(stdout, "OPTIONS:\n"); @@ -1095,6 +1105,7 @@ static void parseArguments(int argc, char* argv[], ParseOptions& options) #endif fprintf(stdout, "\t--mapdirs \n\t\tMap real directories to virtual ones for WASI functions to use.\n\t\tExample: ./walrus test.wasm --mapdirs this/real/directory/ this/virtual/directory\n\n"); fprintf(stdout, "\t--env\n\t\tShare host environment to walrus WASI.\n\n"); + fprintf(stdout, "\t--args [ ... ]\n\t\tRun Webassembly module with arguments: must be followed by the name of the Webassembly module file, then optionally following arguments which are passed on to the module\n\t\tExample: ./walrus --args test.wasm 'hello' 'world' 42\n\n"); exit(0); } } @@ -1117,7 +1128,7 @@ static void parseArguments(int argc, char* argv[], ParseOptions& options) } } -int main(int argc, char* argv[]) +int main(const int argc, const char* argv[]) { #ifndef NDEBUG setbuf(stdout, NULL); @@ -1164,8 +1175,8 @@ int main(int argc, char* argv[]) init_options.out = 1; init_options.err = 2; init_options.fd_table_size = 3; - init_options.argc = 0; - init_options.argv = nullptr; + init_options.argc = (options.argsIndex == -1 ? 0 : argc - options.argsIndex); + init_options.argv = (options.argsIndex == -1 ? nullptr : argv + options.argsIndex); init_options.envp = envp.data(); init_options.preopenc = dirs.size(); init_options.preopens = dirs.data(); diff --git a/src/wasi/WASI.cpp b/src/wasi/WASI.cpp index bfb749886..f5468b30e 100644 --- a/src/wasi/WASI.cpp +++ b/src/wasi/WASI.cpp @@ -214,6 +214,30 @@ void WASI::random_get(ExecutionState& state, Value* argv, Value* result, Instanc result[0] = Value(uvwasi_random_get(WASI::g_uvwasi, buf, length)); } +void WASI::args_get(ExecutionState& state, Value* argv, Value* result, Instance* instance) +{ + uvwasi_size_t argc; + uvwasi_size_t bufSize; + uvwasi_args_sizes_get(WASI::g_uvwasi, &argc, &bufSize); + + char** uvArgv = reinterpret_cast(get_memory_pointer(instance, argv[0], argc * sizeof(char*))); + char* uvArgBuf = reinterpret_cast(get_memory_pointer(instance, argv[1], bufSize)); + + if (uvArgv == nullptr || uvArgBuf == nullptr) { + result[0] = Value(WasiErrNo::inval); + return; + } + + result[0] = Value(static_cast(uvwasi_args_get(WASI::g_uvwasi, uvArgv, uvArgBuf))); +} + +void WASI::args_sizes_get(ExecutionState& state, Value* argv, Value* result, Instance* instance) +{ + uvwasi_size_t* uvArgc = reinterpret_cast(get_memory_pointer(instance, argv[0], sizeof(uint32_t))); + uvwasi_size_t* uvArgvBufSize = reinterpret_cast(get_memory_pointer(instance, argv[1], sizeof(uint32_t))); + + result[0] = Value(static_cast(uvwasi_args_sizes_get(WASI::g_uvwasi, uvArgc, uvArgvBufSize))); +} } // namespace Walrus #endif diff --git a/src/wasi/WASI.h b/src/wasi/WASI.h index 7c3dd8d7e..e44dac1b6 100644 --- a/src/wasi/WASI.h +++ b/src/wasi/WASI.h @@ -47,7 +47,9 @@ class WASI { F(fd_seek, I32I64I32I32_RI32) \ F(path_open, I32I32I32I32I32I64I64I32I32_RI32) \ F(environ_get, I32I32_RI32) \ - F(environ_sizes_get, I32I32_RI32) + F(environ_sizes_get, I32I32_RI32) \ + F(args_get, I32I32_RI32) \ + F(args_sizes_get, I32I32_RI32) #define ERRORS(ERR) \ ERR(success, "no error") \ @@ -164,6 +166,8 @@ class WASI { static void environ_get(ExecutionState& state, Value* argv, Value* result, Instance* instance); static void environ_sizes_get(ExecutionState& state, Value* argv, Value* result, Instance* instance); static void random_get(ExecutionState& state, Value* argv, Value* result, Instance* instance); + static void args_get(ExecutionState& state, Value* argv, Value* result, Instance* instance); + static void args_sizes_get(ExecutionState& state, Value* argv, Value* result, Instance* instance); static uvwasi_t* g_uvwasi; static WasiFuncInfo g_wasiFunctions[FuncEnd]; diff --git a/test/wasi/args.wast b/test/wasi/args.wast new file mode 100644 index 000000000..a4cc50ac6 --- /dev/null +++ b/test/wasi/args.wast @@ -0,0 +1,75 @@ +(module + (import "wasi_snapshot_preview1" "fd_write" (func $wasi_fd_write (param i32 i32 i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "args_get" (func $wasi_args_get (param i32 i32) (result i32))) + (import "wasi_snapshot_preview1" "args_sizes_get" (func $wasi_args_sizes_get (param i32 i32) (result i32))) + (memory 1) + + (export "memory" (memory 0)) + (export "print_args" (func $print_args)) + (data (i32.const 100) "500" ) + + (func $print_args + (local $i i32) + i32.const 0 ;; args count + i32.const 12 ;; args overall size in characters + call $wasi_args_sizes_get + drop + + i32.const 500 ;; argp + i32.const 500 ;; argp[0] + call $wasi_args_get + drop + + ;; Memory + 50 = 500, start of output string. + i32.const 50 + i32.const 500 + i32.store + + ;; Memory + 54 = size of output string. + i32.const 54 + i32.const 12 + i32.load + i32.store + + ;; Replace '\0' with '\n' for readable printing. + i32.const 0 + local.set $i + (loop $loop + i32.const 500 + local.get $i + i32.add + i32.load8_u + + i32.eqz + (if + (then + i32.const 500 + local.get $i + i32.add + i32.const 10 + i32.store8 + ) + ) + + local.get $i + i32.const 1 + i32.add + local.tee $i + + i32.const 12 + i32.load + i32.lt_u + br_if $loop + ) + + (call $wasi_fd_write + (i32.const 1) ;;file descriptor + (i32.const 50) ;;offset of str offset + (i32.const 1) ;;iovec length + (i32.const 200) ;;result offset + ) + drop + ) +) + +(assert_return (invoke "print_args")) diff --git a/tools/run-tests.py b/tools/run-tests.py index 892b04052..df37e135e 100755 --- a/tools/run-tests.py +++ b/tools/run-tests.py @@ -61,7 +61,7 @@ def __call__(self, fn): DEFAULT_RUNNERS.append(self.suite) return fn -def _run_wast_tests(engine, files, is_fail): +def _run_wast_tests(engine, files, is_fail, args=None, expect_stdout=None): fails = 0 for file in files: if jit: @@ -69,14 +69,23 @@ def _run_wast_tests(engine, files, is_fail): if filename in JIT_EXCLUDE_FILES: continue - proc = Popen([engine, "--mapdirs", "./test/wasi", "/var", file], stdout=PIPE) if not jit else Popen([engine, "--mapdirs", "./test/wasi", "/var", "--jit", file], stdout=PIPE) + subprocess_args = [engine, "--mapdirs", "./test/wasi", "/var"] + if jit: subprocess_args.append("--jit") + if args: subprocess_args.append("--args") + subprocess_args.append(file) + if args: subprocess_args.extend(args) + + proc = Popen(subprocess_args, stdout=PIPE) out, _ = proc.communicate() - if is_fail and proc.returncode or not is_fail and not proc.returncode: + if ((is_fail and proc.returncode or not is_fail and not proc.returncode) + and (expect_stdout is None or out == expect_stdout)): print('%sOK: %s%s' % (COLOR_GREEN, file, COLOR_RESET)) else: print('%sFAIL(%d): %s%s' % (COLOR_RED, proc.returncode, file, COLOR_RESET)) - print(out) + print(f"Result: {out}") + if expect_stdout is not None: + print(f"Expected: {expect_stdout}") fails += 1 @@ -136,6 +145,24 @@ def run_wasi_tests(engine): if fail_total > 0: raise Exception("basic wasi tests failed") +@runner('wasi-args', default=True) +def run_wasi_args_tests(engine): + TEST_DIR = join(PROJECT_SOURCE_DIR, 'test', 'wasi') + + print('Running wasi-args tests:') + xpass = glob(join(TEST_DIR, 'args.wast')) + + expected_stdout = f"{xpass[0]}\nHello\nWorld!\n".encode('utf-8') + xpass_result = _run_wast_tests(engine, xpass, False, args=["Hello", "World!"], expect_stdout=expected_stdout) + + tests_total = len(xpass) + fail_total = xpass_result + print('TOTAL: %d' % (tests_total)) + print('%sPASS : %d%s' % (COLOR_GREEN, tests_total - fail_total, COLOR_RESET)) + print('%sFAIL : %d%s' % (COLOR_RED, fail_total, COLOR_RESET)) + + if fail_total > 0: + raise Exception("basic wasi-args tests failed") @runner('jit', default=True) def run_jit_tests(engine):