Compare commits

..

4 Commits

Author SHA1 Message Date
Ayke van Laethem 8e7a288bb4 wasm: use wasip1 API for parameter/env passing
Instead of hardcoding the command line parameters and the environment
variables in the binary, pass them at runtime to Node.js and use the
WASIp1 API to retrieve them in wasm_exec.js.

The only real benefit right now is that it becomes possible to change
`go.argv` and `go.env` before running a wasm binary.

This also changes the syscall package for GOOS=js: it now becomes more
like a libc (using wasi-libc), which means error values like
`syscall.EEXIST` will actually match the one returned by the relevant
libc function.

Wasm binary size for packages that import the os package will be
increased somewhat.
2024-10-30 11:14:18 +01:00
Randy Reddig 0edeaf657f tinygo: revise and simplify wasmtime argument handling (#4555) 2024-10-28 17:57:24 +01:00
Ayke van Laethem 76d5b3d786 sync: don't use volatile in Mutex
Volatile loads/stors are only useful for communication with interrupts
or for memory-mapped I/O. They do not provide any sort of safety for
sync.Mutex, while making it *appear* as if it is more safe.

  * `sync.Mutex` cannot be used safely inside interrupts, because any
    blocking calls (including `Lock`) will cause a runtime panic.
  * For multithreading, `volatile` is also the wrong choice. Atomic
    operations should be used instead, and the current code would not
    work for multithreaded programs anyway.
2024-10-28 16:43:28 +01:00
Ayke van Laethem 915132645e ci: remove 'shell: bash' lines from MacOS build
Unlike Windows, we can just use the default shell here.
2024-10-28 10:22:05 +01:00
18 changed files with 145 additions and 117 deletions
-5
View File
@@ -27,7 +27,6 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Install Dependencies - name: Install Dependencies
shell: bash
run: | run: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu binaryen HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu binaryen
- name: Checkout - name: Checkout
@@ -72,7 +71,6 @@ jobs:
path: llvm-build path: llvm-build
- name: Build LLVM - name: Build LLVM
if: steps.cache-llvm-build.outputs.cache-hit != 'true' if: steps.cache-llvm-build.outputs.cache-hit != 'true'
shell: bash
run: | run: |
# fetch LLVM source # fetch LLVM source
rm -rf llvm-project rm -rf llvm-project
@@ -100,14 +98,12 @@ jobs:
- name: make gen-device - name: make gen-device
run: make -j3 gen-device run: make -j3 gen-device
- name: Test TinyGo - name: Test TinyGo
shell: bash
run: make test GOTESTFLAGS="-short" run: make test GOTESTFLAGS="-short"
- name: Build TinyGo release tarball - name: Build TinyGo release tarball
run: make release -j3 run: make release -j3
- name: Test stdlib packages - name: Test stdlib packages
run: make tinygo-test run: make tinygo-test
- name: Make release artifact - name: Make release artifact
shell: bash
run: cp -p build/release.tar.gz build/tinygo.darwin-${{ matrix.goarch }}.tar.gz run: cp -p build/release.tar.gz build/tinygo.darwin-${{ matrix.goarch }}.tar.gz
- name: Publish release artifact - name: Publish release artifact
# Note: this release artifact is double-zipped, see: # Note: this release artifact is double-zipped, see:
@@ -121,7 +117,6 @@ jobs:
name: darwin-${{ matrix.goarch }}-double-zipped name: darwin-${{ matrix.goarch }}-double-zipped
path: build/tinygo.darwin-${{ matrix.goarch }}.tar.gz path: build/tinygo.darwin-${{ matrix.goarch }}.tar.gz
- name: Smoke tests - name: Smoke tests
shell: bash
run: make smoketest TINYGO=$(PWD)/build/tinygo run: make smoketest TINYGO=$(PWD)/build/tinygo
test-macos-homebrew: test-macos-homebrew:
name: homebrew-install name: homebrew-install
+1 -1
View File
@@ -452,7 +452,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
"--stack-first", "--stack-first",
"--no-demangle", "--no-demangle",
) )
spec.Emulator = "wasmtime --dir={tmpDir}::/tmp {}" spec.Emulator = "wasmtime run --dir={tmpDir}::/tmp {}"
spec.ExtraFiles = append(spec.ExtraFiles, spec.ExtraFiles = append(spec.ExtraFiles,
"src/runtime/asm_tinygowasm.S", "src/runtime/asm_tinygowasm.S",
"src/internal/task/task_asyncify_wasm.S", "src/internal/task/task_asyncify_wasm.S",
+35 -34
View File
@@ -769,22 +769,18 @@ func Run(pkgName string, options *compileopts.Options, cmdArgs []string) error {
// passes command line arguments and environment variables in a way appropriate // passes command line arguments and environment variables in a way appropriate
// for the given emulator. // for the given emulator.
func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, cmdArgs, environmentVars []string, timeout time.Duration, run func(cmd *exec.Cmd, result builder.BuildResult) error) (builder.BuildResult, error) { func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, cmdArgs, environmentVars []string, timeout time.Duration, run func(cmd *exec.Cmd, result builder.BuildResult) error) (builder.BuildResult, error) {
isSingleFile := strings.HasSuffix(pkgName, ".go")
// Determine whether we're on a system that supports environment variables // Determine whether we're on a system that supports environment variables
// and command line parameters (operating systems, WASI) or not (baremetal, // and command line parameters (operating systems, WASI) or not (baremetal).
// WebAssembly in the browser). If we're on a system without an environment, // If we're on a system without an environment, we need to pass command line
// we need to pass command line arguments and environment variables through // arguments and environment variables through global variables (built into
// global variables (built into the binary directly) instead of the // the binary directly) instead of the conventional way.
// conventional way. needsEnvInVars := false
needsEnvInVars := config.GOOS() == "js"
for _, tag := range config.BuildTags() { for _, tag := range config.BuildTags() {
if tag == "baremetal" { if tag == "baremetal" {
needsEnvInVars = true needsEnvInVars = true
} }
} }
var args, emuArgs, env []string var args, env []string
var extraCmdEnv []string var extraCmdEnv []string
if needsEnvInVars { if needsEnvInVars {
runtimeGlobals := make(map[string]string) runtimeGlobals := make(map[string]string)
@@ -804,20 +800,6 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
"runtime": runtimeGlobals, "runtime": runtimeGlobals,
} }
} }
} else if config.EmulatorName() == "wasmtime" {
for _, v := range environmentVars {
emuArgs = append(emuArgs, "--env", v)
}
// Use of '--' argument no longer necessary as of Wasmtime v14:
// https://github.com/bytecodealliance/wasmtime/pull/6946
// args = append(args, "--")
args = append(args, cmdArgs...)
// Set this for nicer backtraces during tests, but don't override the user.
if _, ok := os.LookupEnv("WASMTIME_BACKTRACE_DETAILS"); !ok {
extraCmdEnv = append(extraCmdEnv, "WASMTIME_BACKTRACE_DETAILS=1")
}
} else { } else {
// Pass environment variables and command line parameters as usual. // Pass environment variables and command line parameters as usual.
// This also works on qemu-aarch64 etc. // This also works on qemu-aarch64 etc.
@@ -860,7 +842,7 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
return result, err return result, err
} }
name = emulator[0] name, emulator = emulator[0], emulator[1:]
// wasmtime is a WebAssembly runtime CLI with WASI enabled by default. // wasmtime is a WebAssembly runtime CLI with WASI enabled by default.
// By default, only stdio is allowed. For example, while STDOUT routes // By default, only stdio is allowed. For example, while STDOUT routes
@@ -869,11 +851,24 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
// outside the package directory. Other tests require temporary // outside the package directory. Other tests require temporary
// writeable directories. We allow this by adding wasmtime flags below. // writeable directories. We allow this by adding wasmtime flags below.
if name == "wasmtime" { if name == "wasmtime" {
var emuArgs []string
// Extract the wasmtime subcommand (e.g. "run" or "serve")
if len(emulator) > 1 {
emuArgs = append(emuArgs, emulator[0])
emulator = emulator[1:]
}
wd, _ := os.Getwd()
// Below adds additional wasmtime flags in case a test reads files // Below adds additional wasmtime flags in case a test reads files
// outside its directory, like "../testdata/e.txt". This allows any // outside its directory, like "../testdata/e.txt". This allows any
// relative directory up to the module root, even if the test never // relative directory up to the module root, even if the test never
// reads any files. // reads any files.
if config.TestConfig.CompileTestBinary { if config.TestConfig.CompileTestBinary {
// Set working directory to package dir
wd = result.MainDir
// Add relative dirs (../, ../..) up to module root (for wasip1) // Add relative dirs (../, ../..) up to module root (for wasip1)
dirs := dirsToModuleRootRel(result.MainDir, result.ModuleRoot) dirs := dirsToModuleRootRel(result.MainDir, result.ModuleRoot)
@@ -883,19 +878,25 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
for _, d := range dirs { for _, d := range dirs {
emuArgs = append(emuArgs, "--dir="+d) emuArgs = append(emuArgs, "--dir="+d)
} }
} else {
emuArgs = append(emuArgs, "--dir=.")
} }
dir := result.MainDir emuArgs = append(emuArgs, "--dir="+wd)
if isSingleFile { emuArgs = append(emuArgs, "--env=PWD="+wd)
dir, _ = os.Getwd() for _, v := range environmentVars {
emuArgs = append(emuArgs, "--env", v)
} }
emuArgs = append(emuArgs, "--dir=.")
emuArgs = append(emuArgs, "--dir="+dir) // Set this for nicer backtraces during tests, but don't override the user.
emuArgs = append(emuArgs, "--env=PWD="+dir) if _, ok := os.LookupEnv("WASMTIME_BACKTRACE_DETAILS"); !ok {
extraCmdEnv = append(extraCmdEnv, "WASMTIME_BACKTRACE_DETAILS=1")
}
emulator = append(emuArgs, emulator...)
} }
emuArgs = append(emuArgs, emulator[1:]...) args = append(emulator, args...)
args = append(emuArgs, args...)
} }
var cmd *exec.Cmd var cmd *exec.Cmd
if ctx != nil { if ctx != nil {
@@ -925,7 +926,7 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
// Run binary. // Run binary.
if config.Options.PrintCommands != nil { if config.Options.PrintCommands != nil {
config.Options.PrintCommands(cmd.Path, cmd.Args...) config.Options.PrintCommands(cmd.Path, cmd.Args[1:]...)
} }
err = run(cmd, result) err = run(cmd, result)
if err != nil { if err != nil {
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build baremetal || js || wasm_unknown //go:build baremetal || wasm_unknown
package runtime package runtime
+38
View File
@@ -29,6 +29,44 @@ func proc_exit(exitcode uint32)
//export __stdio_exit //export __stdio_exit
func __stdio_exit() func __stdio_exit()
var args []string
//go:linkname os_runtime_args os.runtime_args
func os_runtime_args() []string {
if args == nil {
// 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)
if argc == 0 {
return nil
}
// 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))
}
}
return args
}
//go:wasmimport wasi_snapshot_preview1 args_get
func args_get(argv *unsafe.Pointer, argv_buf unsafe.Pointer) (errno uint16)
//go:wasmimport wasi_snapshot_preview1 args_sizes_get
func args_sizes_get(argc *uint32, argv_buf_size *uint32) (errno uint16)
const ( const (
putcharBufferSize = 120 putcharBufferSize = 120
stdout = 1 stdout = 1
-42
View File
@@ -2,10 +2,6 @@
package runtime package runtime
import (
"unsafe"
)
type timeUnit int64 type timeUnit int64
// libc constructors // libc constructors
@@ -21,38 +17,6 @@ func init() {
__wasm_call_ctors() __wasm_call_ctors()
} }
var args []string
//go:linkname os_runtime_args os.runtime_args
func os_runtime_args() []string {
if args == nil {
// 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)
if argc == 0 {
return nil
}
// 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))
}
}
return args
}
func ticksToNanoseconds(ticks timeUnit) int64 { func ticksToNanoseconds(ticks timeUnit) int64 {
return int64(ticks) return int64(ticks)
} }
@@ -97,12 +61,6 @@ func beforeExit() {
// Implementations of WASI APIs // Implementations of WASI APIs
//go:wasmimport wasi_snapshot_preview1 args_get
func args_get(argv *unsafe.Pointer, argv_buf unsafe.Pointer) (errno uint16)
//go:wasmimport wasi_snapshot_preview1 args_sizes_get
func args_sizes_get(argc *uint32, argv_buf_size *uint32) (errno uint16)
//go:wasmimport wasi_snapshot_preview1 clock_time_get //go:wasmimport wasi_snapshot_preview1 clock_time_get
func clock_time_get(clockid uint32, precision uint64, time *uint64) (errno uint16) func clock_time_get(clockid uint32, precision uint64, time *uint64) (errno uint16)
+6 -23
View File
@@ -3,12 +3,10 @@ package sync
import ( import (
"internal/task" "internal/task"
_ "unsafe" _ "unsafe"
"runtime/volatile"
) )
type Mutex struct { type Mutex struct {
state uint8 // Set to non-zero if locked. locked bool
blocked task.Stack blocked task.Stack
} }
@@ -16,18 +14,18 @@ type Mutex struct {
func scheduleTask(*task.Task) func scheduleTask(*task.Task)
func (m *Mutex) Lock() { func (m *Mutex) Lock() {
if m.islocked() { if m.locked {
// Push self onto stack of blocked tasks, and wait to be resumed. // Push self onto stack of blocked tasks, and wait to be resumed.
m.blocked.Push(task.Current()) m.blocked.Push(task.Current())
task.Pause() task.Pause()
return return
} }
m.setlock(true) m.locked = true
} }
func (m *Mutex) Unlock() { func (m *Mutex) Unlock() {
if !m.islocked() { if !m.locked {
panic("sync: unlock of unlocked Mutex") panic("sync: unlock of unlocked Mutex")
} }
@@ -35,7 +33,7 @@ func (m *Mutex) Unlock() {
if t := m.blocked.Pop(); t != nil { if t := m.blocked.Pop(); t != nil {
scheduleTask(t) scheduleTask(t)
} else { } else {
m.setlock(false) m.locked = false
} }
} }
@@ -45,28 +43,13 @@ func (m *Mutex) Unlock() {
// and use of TryLock is often a sign of a deeper problem // and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes. // in a particular use of mutexes.
func (m *Mutex) TryLock() bool { func (m *Mutex) TryLock() bool {
if m.islocked() { if m.locked {
return false return false
} }
m.Lock() m.Lock()
return true return true
} }
func (m *Mutex) islocked() bool {
return volatile.LoadUint8(&m.state) != 0
}
func (m *Mutex) setlock(b bool) {
volatile.StoreUint8(&m.state, boolToU8(b))
}
func boolToU8(b bool) uint8 {
if b {
return 1
}
return 0
}
type RWMutex struct { type RWMutex struct {
// waitingWriters are all of the tasks waiting for write locks. // waitingWriters are all of the tasks waiting for write locks.
waitingWriters task.Stack waitingWriters task.Stack
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build nintendoswitch || wasip1 //go:build js || nintendoswitch || wasip1
package syscall package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !wasip1 && !wasip2 //go:build !js && !wasip1 && !wasip2
package syscall package syscall
@@ -1,4 +1,4 @@
//go:build wasip1 //go:build js || wasip1
package syscall package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build nintendoswitch || wasip1 || wasip2 //go:build js || nintendoswitch || wasip1 || wasip2
package syscall package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build wasip1 || wasip2 //go:build js || wasip1 || wasip2
package syscall package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build baremetal || js || wasm_unknown //go:build baremetal || wasm_unknown
package syscall package syscall
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build baremetal || nintendoswitch || js || wasm_unknown //go:build baremetal || nintendoswitch || wasm_unknown
package syscall package syscall
+1 -1
View File
@@ -23,5 +23,5 @@
"extra-files": [ "extra-files": [
"src/runtime/asm_tinygowasm.S" "src/runtime/asm_tinygowasm.S"
], ],
"emulator": "wasmtime --dir={tmpDir}::/tmp {}" "emulator": "wasmtime run --dir={tmpDir}::/tmp {}"
} }
+1 -1
View File
@@ -25,7 +25,7 @@
"extra-files": [ "extra-files": [
"src/runtime/asm_tinygowasm.S" "src/runtime/asm_tinygowasm.S"
], ],
"emulator": "wasmtime --wasm component-model -Sinherit-network -Sallow-ip-name-lookup --dir={tmpDir}::/tmp {}", "emulator": "wasmtime run --wasm component-model -Sinherit-network -Sallow-ip-name-lookup --dir={tmpDir}::/tmp {}",
"wit-package": "{root}/lib/wasi-cli/wit/", "wit-package": "{root}/lib/wasi-cli/wit/",
"wit-world": "wasi:cli/command" "wit-world": "wasi:cli/command"
} }
+1 -1
View File
@@ -24,5 +24,5 @@
"extra-files": [ "extra-files": [
"src/runtime/asm_tinygowasm.S" "src/runtime/asm_tinygowasm.S"
], ],
"emulator": "wasmtime --dir={tmpDir}::/tmp {}" "emulator": "wasmtime run --dir={tmpDir}::/tmp {}"
} }
+54 -1
View File
@@ -135,6 +135,8 @@
global.Go = class { global.Go = class {
constructor() { constructor() {
this.argv = ["js"];
this.env = {};
this._callbackTimeouts = new Map(); this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1; this._nextCallbackTimeoutID = 1;
@@ -235,9 +237,58 @@
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len));
} }
const storeStringArraySizes = (array, num_ptr, buf_size_ptr) => {
let buf_size = 0;
for (let s of array) {
buf_size += s.length + 1;
}
mem().setUint32(num_ptr, array.length, true);
mem().setUint32(buf_size_ptr, buf_size, true);
}
const storeStringArray = (array, ptrs_ptr, buf_ptr) => {
for (let s of array) {
// Put string data in buffer.
let data = encoder.encode(s);
let dest = new Uint8Array(this._inst.exports.memory.buffer, buf_ptr, data.length);
dest.set(data);
mem().setUint8(buf_ptr+data.length, 0);
// Put pointer to buffer in pointers array.
mem().setUint32(ptrs_ptr, buf_ptr, true);
// Advance to the next element in the array.
ptrs_ptr += 4;
buf_ptr += data.length + 1;
}
}
const envArray = () => {
let array = [];
for (let [key, value] of Object.entries(this.env)) {
array.push(`${key}=${value}`);
}
return array;
}
const timeOrigin = Date.now() - performance.now(); const timeOrigin = Date.now() - performance.now();
this.importObject = { this.importObject = {
wasi_snapshot_preview1: { wasi_snapshot_preview1: {
args_sizes_get: (argc_ptr, argv_buf_size_ptr) => {
storeStringArraySizes(this.argv, argc_ptr, argv_buf_size_ptr);
return 0;
},
args_get: (argv_ptr, argv_buf_ptr) => {
storeStringArray(this.argv, argv_ptr, argv_buf_ptr);
return 0;
},
environ_get: (env_ptr, env_buf_ptr) => {
storeStringArray(envArray(), env_ptr, env_buf_ptr);
},
environ_sizes_get: (env_ptr, env_buf_size_ptr) => {
storeStringArraySizes(envArray(), env_ptr, env_buf_size_ptr);
return 0;
},
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0; let nwritten = 0;
@@ -504,12 +555,14 @@
global.process.versions && global.process.versions &&
!global.process.versions.electron !global.process.versions.electron
) { ) {
if (process.argv.length != 3) { if (process.argv.length < 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1); process.exit(1);
} }
const go = new Go(); const go = new Go();
go.argv = process.argv.slice(2);
go.env = process.env;
WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => {
return go.run(result.instance); return go.run(result.instance);
}).catch((err) => { }).catch((err) => {