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 }}
steps:
- name: Install Dependencies
shell: bash
run: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install qemu binaryen
- name: Checkout
@@ -72,7 +71,6 @@ jobs:
path: llvm-build
- name: Build LLVM
if: steps.cache-llvm-build.outputs.cache-hit != 'true'
shell: bash
run: |
# fetch LLVM source
rm -rf llvm-project
@@ -100,14 +98,12 @@ jobs:
- name: make gen-device
run: make -j3 gen-device
- name: Test TinyGo
shell: bash
run: make test GOTESTFLAGS="-short"
- name: Build TinyGo release tarball
run: make release -j3
- name: Test stdlib packages
run: make tinygo-test
- name: Make release artifact
shell: bash
run: cp -p build/release.tar.gz build/tinygo.darwin-${{ matrix.goarch }}.tar.gz
- name: Publish release artifact
# Note: this release artifact is double-zipped, see:
@@ -121,7 +117,6 @@ jobs:
name: darwin-${{ matrix.goarch }}-double-zipped
path: build/tinygo.darwin-${{ matrix.goarch }}.tar.gz
- name: Smoke tests
shell: bash
run: make smoketest TINYGO=$(PWD)/build/tinygo
test-macos-homebrew:
name: homebrew-install
+1 -1
View File
@@ -452,7 +452,7 @@ func defaultTarget(options *Options) (*TargetSpec, error) {
"--stack-first",
"--no-demangle",
)
spec.Emulator = "wasmtime --dir={tmpDir}::/tmp {}"
spec.Emulator = "wasmtime run --dir={tmpDir}::/tmp {}"
spec.ExtraFiles = append(spec.ExtraFiles,
"src/runtime/asm_tinygowasm.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
// 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) {
isSingleFile := strings.HasSuffix(pkgName, ".go")
// Determine whether we're on a system that supports environment variables
// and command line parameters (operating systems, WASI) or not (baremetal,
// WebAssembly in the browser). If we're on a system without an environment,
// we need to pass command line arguments and environment variables through
// global variables (built into the binary directly) instead of the
// conventional way.
needsEnvInVars := config.GOOS() == "js"
// and command line parameters (operating systems, WASI) or not (baremetal).
// If we're on a system without an environment, we need to pass command line
// arguments and environment variables through global variables (built into
// the binary directly) instead of the conventional way.
needsEnvInVars := false
for _, tag := range config.BuildTags() {
if tag == "baremetal" {
needsEnvInVars = true
}
}
var args, emuArgs, env []string
var args, env []string
var extraCmdEnv []string
if needsEnvInVars {
runtimeGlobals := make(map[string]string)
@@ -804,20 +800,6 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
"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 {
// Pass environment variables and command line parameters as usual.
// 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
}
name = emulator[0]
name, emulator = emulator[0], emulator[1:]
// wasmtime is a WebAssembly runtime CLI with WASI enabled by default.
// 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
// writeable directories. We allow this by adding wasmtime flags below.
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
// outside its directory, like "../testdata/e.txt". This allows any
// relative directory up to the module root, even if the test never
// reads any files.
if config.TestConfig.CompileTestBinary {
// Set working directory to package dir
wd = result.MainDir
// Add relative dirs (../, ../..) up to module root (for wasip1)
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 {
emuArgs = append(emuArgs, "--dir="+d)
}
} else {
emuArgs = append(emuArgs, "--dir=.")
}
dir := result.MainDir
if isSingleFile {
dir, _ = os.Getwd()
emuArgs = append(emuArgs, "--dir="+wd)
emuArgs = append(emuArgs, "--env=PWD="+wd)
for _, v := range environmentVars {
emuArgs = append(emuArgs, "--env", v)
}
emuArgs = append(emuArgs, "--dir=.")
emuArgs = append(emuArgs, "--dir="+dir)
emuArgs = append(emuArgs, "--env=PWD="+dir)
// 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")
}
emulator = append(emuArgs, emulator...)
}
emuArgs = append(emuArgs, emulator[1:]...)
args = append(emuArgs, args...)
args = append(emulator, args...)
}
var cmd *exec.Cmd
if ctx != nil {
@@ -925,7 +926,7 @@ func buildAndRun(pkgName string, config *compileopts.Config, stdout io.Writer, c
// Run binary.
if config.Options.PrintCommands != nil {
config.Options.PrintCommands(cmd.Path, cmd.Args...)
config.Options.PrintCommands(cmd.Path, cmd.Args[1:]...)
}
err = run(cmd, result)
if err != nil {
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build baremetal || js || wasm_unknown
//go:build baremetal || wasm_unknown
package runtime
+38
View File
@@ -29,6 +29,44 @@ func proc_exit(exitcode uint32)
//export __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 (
putcharBufferSize = 120
stdout = 1
-42
View File
@@ -2,10 +2,6 @@
package runtime
import (
"unsafe"
)
type timeUnit int64
// libc constructors
@@ -21,38 +17,6 @@ func init() {
__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 {
return int64(ticks)
}
@@ -97,12 +61,6 @@ func beforeExit() {
// 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
func clock_time_get(clockid uint32, precision uint64, time *uint64) (errno uint16)
+6 -23
View File
@@ -3,12 +3,10 @@ package sync
import (
"internal/task"
_ "unsafe"
"runtime/volatile"
)
type Mutex struct {
state uint8 // Set to non-zero if locked.
locked bool
blocked task.Stack
}
@@ -16,18 +14,18 @@ type Mutex struct {
func scheduleTask(*task.Task)
func (m *Mutex) Lock() {
if m.islocked() {
if m.locked {
// Push self onto stack of blocked tasks, and wait to be resumed.
m.blocked.Push(task.Current())
task.Pause()
return
}
m.setlock(true)
m.locked = true
}
func (m *Mutex) Unlock() {
if !m.islocked() {
if !m.locked {
panic("sync: unlock of unlocked Mutex")
}
@@ -35,7 +33,7 @@ func (m *Mutex) Unlock() {
if t := m.blocked.Pop(); t != nil {
scheduleTask(t)
} 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
// in a particular use of mutexes.
func (m *Mutex) TryLock() bool {
if m.islocked() {
if m.locked {
return false
}
m.Lock()
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 {
// waitingWriters are all of the tasks waiting for write locks.
waitingWriters task.Stack
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build nintendoswitch || wasip1
//go:build js || nintendoswitch || wasip1
package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !wasip1 && !wasip2
//go:build !js && !wasip1 && !wasip2
package syscall
@@ -1,4 +1,4 @@
//go:build wasip1
//go:build js || wasip1
package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build nintendoswitch || wasip1 || wasip2
//go:build js || nintendoswitch || wasip1 || wasip2
package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build wasip1 || wasip2
//go:build js || wasip1 || wasip2
package syscall
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build baremetal || js || wasm_unknown
//go:build baremetal || wasm_unknown
package syscall
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build baremetal || nintendoswitch || js || wasm_unknown
//go:build baremetal || nintendoswitch || wasm_unknown
package syscall
+1 -1
View File
@@ -23,5 +23,5 @@
"extra-files": [
"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": [
"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-world": "wasi:cli/command"
}
+1 -1
View File
@@ -24,5 +24,5 @@
"extra-files": [
"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 {
constructor() {
this.argv = ["js"];
this.env = {};
this._callbackTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
@@ -235,9 +237,58 @@
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();
this.importObject = {
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
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) {
let nwritten = 0;
@@ -504,12 +555,14 @@
global.process.versions &&
!global.process.versions.electron
) {
if (process.argv.length != 3) {
if (process.argv.length < 3) {
console.error("usage: go_js_wasm_exec [wasm binary] [arguments]");
process.exit(1);
}
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) => {
return go.run(result.instance);
}).catch((err) => {