diff options
author | Ayke van Laethem <[email protected]> | 2021-04-16 15:20:20 +0200 |
---|---|---|
committer | Ron Evans <[email protected]> | 2021-04-21 10:32:09 +0200 |
commit | 7b761fac7805083dd0263ed376b10e106726ef8f (patch) | |
tree | 4a984aeda71f2b902ba14b1d2b6730f9d862187e | |
parent | c47cdfa66fe38cfad268615e093969bb136bae07 (diff) | |
download | tinygo-7b761fac7805083dd0263ed376b10e106726ef8f.tar.gz tinygo-7b761fac7805083dd0263ed376b10e106726ef8f.zip |
runtime: implement command line arguments in hosted environments
Implement command line arguments for Linux, MacOS and WASI.
-rw-r--r-- | interp/interpreter.go | 5 | ||||
-rw-r--r-- | main_test.go | 24 | ||||
-rw-r--r-- | src/runtime/runtime.go | 7 | ||||
-rw-r--r-- | src/runtime/runtime_unix.go | 30 | ||||
-rw-r--r-- | src/runtime/runtime_wasm_js.go | 5 | ||||
-rw-r--r-- | src/runtime/runtime_wasm_wasi.go | 37 | ||||
-rw-r--r-- | testdata/env.go | 9 | ||||
-rw-r--r-- | testdata/env.txt | 3 |
8 files changed, 102 insertions, 18 deletions
diff --git a/interp/interpreter.go b/interp/interpreter.go index 71be6957c..ec71c56e0 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -196,7 +196,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // which case this call won't even get to this point but will // already be emitted in initAll. continue - case strings.HasPrefix(callFn.name, "runtime.print") || callFn.name == "runtime._panic" || callFn.name == "runtime.hashmapGet": + case strings.HasPrefix(callFn.name, "runtime.print") || callFn.name == "runtime._panic" || callFn.name == "runtime.hashmapGet" || callFn.name == "os.runtime_args": // These functions should be run at runtime. Specifically: // * Print and panic functions are best emitted directly without // interpreting them, otherwise we get a ton of putchar (etc.) @@ -204,6 +204,9 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // * runtime.hashmapGet tries to access the map value directly. // This is not possible as the map value is treated as a special // kind of object in this package. + // * os.runtime_args reads globals that are initialized outside + // the view of the interp package so it always needs to be run + // at runtime. err := r.runAtRuntime(fn, inst, locals, &mem, indent) if err != nil { return nil, mem, err diff --git a/main_test.go b/main_test.go index c9b02ba8c..224a71b0b 100644 --- a/main_test.go +++ b/main_test.go @@ -127,7 +127,7 @@ func TestCompiler(t *testing.T) { t.Parallel() runTestWithConfig("stdlib.go", "", t, &compileopts.Options{ Opt: "1", - }, nil) + }, nil, nil) }) // Test with only the bare minimum of optimizations enabled. @@ -136,7 +136,7 @@ func TestCompiler(t *testing.T) { t.Parallel() runTestWithConfig("print.go", "", t, &compileopts.Options{ Opt: "0", - }, nil) + }, nil, nil) }) t.Run("ldflags", func(t *testing.T) { @@ -148,7 +148,7 @@ func TestCompiler(t *testing.T) { "someGlobal": "foobar", }, }, - }, nil) + }, nil, nil) }) }) } @@ -160,17 +160,17 @@ func runPlatTests(target string, tests []string, t *testing.T) { name := name // redefine to avoid race condition t.Run(name, func(t *testing.T) { t.Parallel() - runTest(name, target, t, nil) + runTest(name, target, t, nil, nil) }) } if target == "wasi" || target == "" { t.Run("filesystem.go", func(t *testing.T) { t.Parallel() - runTest("filesystem.go", target, t, nil) + runTest("filesystem.go", target, t, nil, nil) }) t.Run("env.go", func(t *testing.T) { t.Parallel() - runTest("env.go", target, t, []string{"ENV1=VALUE1", "ENV2=VALUE2"}) + runTest("env.go", target, t, []string{"first", "second"}, []string{"ENV1=VALUE1", "ENV2=VALUE2"}) }) } } @@ -187,7 +187,7 @@ func runBuild(src, out string, opts *compileopts.Options) error { return Build(src, out, opts) } -func runTest(name, target string, t *testing.T, environmentVars []string) { +func runTest(name, target string, t *testing.T, cmdArgs, environmentVars []string) { options := &compileopts.Options{ Target: target, Opt: "z", @@ -198,10 +198,10 @@ func runTest(name, target string, t *testing.T, environmentVars []string) { PrintSizes: "", WasmAbi: "", } - runTestWithConfig(name, target, t, options, environmentVars) + runTestWithConfig(name, target, t, options, cmdArgs, environmentVars) } -func runTestWithConfig(name, target string, t *testing.T, options *compileopts.Options, environmentVars []string) { +func runTestWithConfig(name, target string, t *testing.T, options *compileopts.Options, cmdArgs, environmentVars []string) { // Get the expected output for this test. // Note: not using filepath.Join as it strips the path separator at the end // of the path. @@ -244,6 +244,7 @@ func runTestWithConfig(name, target string, t *testing.T, options *compileopts.O if target == "" { cmd = exec.Command(binary) cmd.Env = append(cmd.Env, environmentVars...) + cmd.Args = append(cmd.Args, cmdArgs...) } else { spec, err := compileopts.LoadTarget(target) if err != nil { @@ -257,11 +258,12 @@ func runTestWithConfig(name, target string, t *testing.T, options *compileopts.O } if len(spec.Emulator) != 0 && spec.Emulator[0] == "wasmtime" { + // Allow reading from the current directory. + cmd.Args = append(cmd.Args, "--dir=.") for _, v := range environmentVars { cmd.Args = append(cmd.Args, "--env", v) } - // Allow reading from the current directory. - cmd.Args = append(cmd.Args, "--dir=.") + cmd.Args = append(cmd.Args, cmdArgs...) } else { cmd.Env = append(cmd.Env, environmentVars...) } diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 1bc09ce0a..7b1b4f5ae 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -23,7 +23,9 @@ func GOROOT() string { return "/usr/local/go" } -// TODO: fill with real args. +// This is the default set of arguments, if nothing else has been set. +// This may be overriden by modifying this global at runtime init (for example, +// on Linux where there are real command line arguments). var args = []string{"/proc/self/exe"} //go:linkname os_runtime_args os.runtime_args @@ -47,6 +49,9 @@ func memmove(dst, src unsafe.Pointer, size uintptr) // llvm.memset.p0i8.i32(ptr, 0, size, false). func memzero(ptr unsafe.Pointer, size uintptr) +//export strlen +func strlen(ptr unsafe.Pointer) uintptr + // Compare two same-size buffers for equality. func memequal(x, y unsafe.Pointer, n uintptr) bool { for i := uintptr(0); i < n; i++ { diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index c0a0ec63c..dc6d74856 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -43,9 +43,34 @@ func postinit() {} // Entry point for Go. Initialize all packages and call main.main(). //export main -func main() int { +func main(argc int32, argv *unsafe.Pointer) int { preinit() + // Make args global big enough so that it can store all command line + // arguments. Unfortunately this has to be done with some magic as the heap + // is not yet initialized. + argsSlice := (*struct { + ptr unsafe.Pointer + len uintptr + cap uintptr + })(unsafe.Pointer(&args)) + argsSlice.ptr = malloc(uintptr(argc) * (unsafe.Sizeof(uintptr(0))) * 3) + argsSlice.len = 0 + argsSlice.cap = uintptr(argc) + + // Initialize command line parameters. + for *argv != nil { + // Convert the C string to a Go string. + length := strlen(*argv) + argString := _string{ + length: length, + ptr: (*byte)(*argv), + } + args = append(args, *(*string)(unsafe.Pointer(&argString))) + // This is the Go equivalent of "argc++" in C. + argv = (*unsafe.Pointer)(unsafe.Pointer(uintptr(unsafe.Pointer(argv)) + unsafe.Sizeof(argv))) + } + // Obtain the initial stack pointer right before calling the run() function. // The run function has been moved to a separate (non-inlined) function so // that the correct stack pointer is read. @@ -65,9 +90,6 @@ func runMain() { //go:extern environ var environ *unsafe.Pointer -//export strlen -func strlen(ptr unsafe.Pointer) uintptr - //go:linkname syscall_runtime_envs syscall.runtime_envs func syscall_runtime_envs() []string { // Count how many environment variables there are. diff --git a/src/runtime/runtime_wasm_js.go b/src/runtime/runtime_wasm_js.go index ea1cc6e41..5e2eb1f51 100644 --- a/src/runtime/runtime_wasm_js.go +++ b/src/runtime/runtime_wasm_js.go @@ -15,6 +15,11 @@ func _start() { run() } +//go:linkname syscall_runtime_envs syscall.runtime_envs +func syscall_runtime_envs() []string { + return nil +} + var handleEvent func() //go:linkname setEventHandler syscall/js.setEventHandler diff --git a/src/runtime/runtime_wasm_wasi.go b/src/runtime/runtime_wasm_wasi.go index 97659a10d..02fa02601 100644 --- a/src/runtime/runtime_wasm_wasi.go +++ b/src/runtime/runtime_wasm_wasi.go @@ -21,6 +21,33 @@ func _start() { run() } +// Read the command line arguments from WASI. +// For example, they can be passed to a program with wasmtime like this: +// +// wasmtime ./program.wasm arg1 arg2 +func init() { + // Read the number of args (argc) and the buffer size required to store all + // these args (argv). + var argc, argv_buf_size uint32 + args_sizes_get(&argc, &argv_buf_size) + + // Obtain the command line arguments + argsSlice := make([]unsafe.Pointer, argc) + buf := make([]byte, argv_buf_size) + args_get(&argsSlice[0], unsafe.Pointer(&buf[0])) + + // Convert the array of C strings to an array of Go strings. + args = make([]string, argc) + for i, cstr := range argsSlice { + length := strlen(cstr) + argString := _string{ + length: length, + ptr: (*byte)(cstr), + } + args[i] = *(*string)(unsafe.Pointer(&argString)) + } +} + func ticksToNanoseconds(ticks timeUnit) int64 { return int64(ticks) } @@ -62,7 +89,15 @@ func ticks() timeUnit { return timeUnit(nano) } -// Implementations of wasi_unstable APIs +// Implementations of WASI APIs + +//go:wasm-module wasi_snapshot_preview1 +//export args_get +func args_get(argv *unsafe.Pointer, argv_buf unsafe.Pointer) (errno uint16) + +//go:wasm-module wasi_snapshot_preview1 +//export args_sizes_get +func args_sizes_get(argc *uint32, argv_buf_size *uint32) (errno uint16) //go:wasm-module wasi_snapshot_preview1 //export clock_time_get diff --git a/testdata/env.go b/testdata/env.go index 74d271946..115da6d3d 100644 --- a/testdata/env.go +++ b/testdata/env.go @@ -5,10 +5,19 @@ import ( ) func main() { + // Check for environment variables (set by the test runner). println("ENV1:", os.Getenv("ENV1")) v, ok := os.LookupEnv("ENV2") if !ok { println("ENV2 not found") } println("ENV2:", v) + + // Check for command line arguments. + // Argument 0 is skipped because it is the program name, which varies by + // test run. + println() + for _, arg := range os.Args[1:] { + println("arg:", arg) + } } diff --git a/testdata/env.txt b/testdata/env.txt index 7aadfb7b4..8ba50a7fd 100644 --- a/testdata/env.txt +++ b/testdata/env.txt @@ -1,2 +1,5 @@ ENV1: VALUE1 ENV2: VALUE2 + +arg: first +arg: second |