Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/compile: add go:wasmexport directive #65199

Closed
johanbrandhorst opened this issue Jan 22, 2024 · 117 comments
Closed

cmd/compile: add go:wasmexport directive #65199

johanbrandhorst opened this issue Jan 22, 2024 · 117 comments
Milestone

Comments

@johanbrandhorst
Copy link
Member

johanbrandhorst commented Jan 22, 2024

Background

#38248 defined a new compiler directive, go:wasmimport, for interfacing with host defined functions. This allowed calling from Go code into host functions, but it’s still not possible to call from the WebAssembly (Wasm) host into Go code.

Some applications have adopted the practice of allowing them to be extended by calling into Wasm compiled code according to some well defined ABI. Examples include Envoy, Istio, VS Code and others. Go cannot support compiling code to these applications, as the only exported function in the module compiled by Go is _start, mapping to the main function in a main package.

Despite this, some users are designing custom plugin systems using this interface, utilizing standard in and standard out for communicating with the Wasm binary. This shows a desire for exporting Go functions in the community.

There have been historical discussions on implementing this before (including #42372, #25612 and #41715), but none of them have reached a consensus on a design and implementation. In particular, #42372 had a long discussion (and design doc) that never provided a satisfying answer for how to run executed functions in the Go runtime. Instead of reviving that discussion, this proposal will attempt to build on it and answer the questions posed. This proposal supersedes #42372.

Exporting functions to the wasm host is also a necessity for a hypothetical GOOS=wasip2 targeting preview 2 of the WASI specification. This could be implemented as a special case in the compiler but since this is a feature requested by users it could reuse that functionality (similar to go:wasmimport today).

Proposal

Repurpose the -buildmode build flag value c-shared for the wasip1 port. It now signals to the compiler to replace the _start function with an _initialize function, which performs runtime and package initialization.

Add a new compiler directive, go:wasmexport, which is used to signal to the compiler that a function should be exported using a Wasm export in the resulting Wasm binary. Using the compiler directive will result in a compilation failure unless the target GOOS is wasip1.

There is a single optional required parameter to the directive, defining the name of the exported function:
(UPDATE: make the parameter required, consistent with the //export pragma and easier to implement).

//go:wasmexport name

The directive is only allowed on functions, not methods.

Discussion

Parallel with -buildmode=c-shared and CGO

The proposed implementation is inspired by the implementation of C references to Go functions. When an exported function is called, a new goroutine (G) is created, which executes on a single thread (M), since Wasm is a single threaded architecture. The runtime will wake up and resume scheduling goroutines as necessary, with the exported function being one of the goroutines available for scheduling. Any other goroutines started during package initialization or left over from previous exported function executions will also be available for scheduling.

Why a "-buildmode" option?

The wasi_snapshot_preview1 documentation states that a _start function and an _initialize function are mutually exclusive. Additionally, at the end of the current _start functions as compiled by Go, proc_exit is called. At this point, the module is considered done, and cannot be interacted with. Given these conditions, we need some way for a user to declare that they want to build a binary especially for exporting one or more functions and to include the _initialize function for package and runtime initialization.

We also considered using a GOWASM option instead, but this feels wrong since that environment variable is used to specify options relating to the architecture (existing options are satconv and signext), while this export option is dependent on the behavior of the "OS" (what functions to export, what initialization pattern to expect).

What happens to func main when exports are involved?

Go code compiled to a wasip1 Wasm binary can be either a "Command", which includes the _start function, or a "Reactor/Library", which includes the _initialize function.

When using -buildmode=c-shared, the resulting Wasm binary will not contain a _start function, and will only contain the _initialize function and any exported functions. The Go main function will not be exported to the host. The user can choose to export it like any other function using the //go:wasmexport directive. The _initialize function will not automatically call main. The main function will not initialize the runtime.

When the -buildmode flag is unset, the _start function and any exported functions will be exported to the host. Using //go:wasmexport on the main function in this mode will result in a compilation error. In this mode, only _start will initialize the runtime, and so must be the first export called from the host. Any other exported functions may only be called through calling into host functions that call other exports during the execution of the _start function. Once the _start function has returned, no other exports may be called on the same instance.

Why not reuse //export?

//export is used to export Go functions to C when using buildmode=c-shared. Use of //export puts restrictions on the use of the file, namely that it cannot contain definitions, only declarations. It’s also something of an ugly duckling among compiler directives in that it doesn’t use the now established go: prefix. A new directive removes the need for users to define functions separately from the declaration, has a nice symmetry with go:wasmimport, and uses the well established go: prefix.

Handling Reentrant Calls and Panics

Reentrant calls happen when the Go application calls a host import, and that invocation calls back into an exported function. Reentrant calls are handled by creating a new goroutine. If a panic reaches the top-level of the go:wasmexport call, the program crashes because there are no mechanisms allowing the guest application to propagate the panic to the Wasm host.

Naming exports

When the name of the Go function matches that of the desired Wasm export, the name parameter can be omitted.

For example:

//go:wasmexport add
func add(x, y int) int {
    return x + y
}

Is equivalent to

//go:wasmexport
func add(x, y int) int {
    return x + y
}

The names _start and _initialize are reserved and not available for user exported functions.

Third-party libraries

Third-party libraries will need to be able to define exports, as WASI functionality such as wasi-http requires calling into exported functions, which would be provided by the third party library in a user-friendly wrapper. Any exports defined in third party libraries are compiled to exported Wasm functions.

Module names

The current Wasm architecture doesn’t define a module name of the compiled module, and this proposal does not suggest adding one. Module names are useful to namespace different compiled Wasm binaries, but it can usually be configured by the runtime or using post-processing tools on the binaries. Future proposals may suggest some way to build this into the Go build system, but this proposal suggests not naming it for simplicity.

Conflicting exports

If the compiler detects multiple exports using the same name, a compile error will occur and warn the user that multiple definitions are in conflict. This may have to happen at link time. If this happens in third-party libraries the user has no recourse but to avoid using one of the libraries.

Supported Types

The go:wasmimport directive allows the declaration of host imports by naming the module and function that the application depends on. The directive applies restrictions on the types that can be used in the function signatures, limiting to fixed-size integers and floats, and unsafe.Pointer, which allows simple mapping rules between the Go and Wasm types. The go:wasmexport directive will use the same type restrictions. Any future relaxing of this restriction will be subject to a separate proposal.

Spawning Goroutines from go:wasmexport functions

The proposal considers scenarios where the go:wasmexport call spawns new goroutines. In the absence of threading or stack switching capability in Wasm, the simplest option is to document that all goroutines still running when the invocation of the go:wasmexport function returns will be paused until the control flow re-enters the Go application.

In the future, we anticipate that Wasm will gain the ability to either spawn threads or integrate with the event loop of the host runtime (e.g., via stack-switching) to drive background goroutines to completion after the invocation of a go:wasmexport function has returned.

Blocking in go:wasmexport functions

When the goroutine running the exported function blocks for any reason, the function will yield to the Go runtime. The Go runtime will schedule other goroutines as necessary. If there are no other goroutines, the application will crash with a deadlock, as there is no way to proceed, and Wasm code cannot block.

Authors

@johanbrandhorst, @achille-roussel, @Pryz, @dgryski, @evanphx, @neelance, @mdlayher

Acknowledgements

Thanks to all participants in the go:wasmexport discussion at the Go contributor summit at GopherCon 2023, without which this proposal would not have been possible.

CC @golang/wasm @cherrymui

@ydnar
Copy link

ydnar commented Jan 22, 2024

Thanks for putting this together—this is exciting.

Generating a module that can act as a reactor and a command sounds like a great idea. I noticed this might conflict with how Node interprets a module. If a module exports both _start and _initialize, it will throw an exception: https://nodejs.org/api/wasi.html

  1. One could argue this is undesired behavior, and Node could change.
  2. What happens if a host detects and calls both _initialize and _start?
  3. Exporting one or the other, but not both, implies some kind of configuration or detection.

@ydnar
Copy link

ydnar commented Jan 22, 2024

The directive is only allowed on functions, not methods.

Using //go:wasmimport on methods has been helpful for mapping Component Model resource methods in WASI Preview 2:

From https://github.com/ydnar/wasm-tools-go/blob/9b4707e054a8b528b27240cba6c05557c4e26a53/wasi/io/error/error.wit.go:

// ToDebugString represents the method "wasi:io/error.error#to-debug-string".
//
// Returns a string that is suitable to assist humans in debugging
// this error.
//
// WARNING: The returned string should not be consumed mechanically!
// It may change across platforms, hosts, or other implementation
// details. Parsing this string is a major platform-compatibility
// hazard.
func (self Error) ToDebugString() string {
	var ret string
	self.to_debug_string(&ret)
	return ret
}

//go:wasmimport wasi:io/error@0.2.0-rc-2023-11-10 [method]error.to-debug-string
func (self Error) to_debug_string(ret *string)

Subjectively, using methods seems better aligned with the Component Model semantics than the equivalent:

//go:wasmimport wasi:io/error@0.2.0-rc-2023-11-10 [method]error.to-debug-string
func error__to_debug_string(self Error, ret *string)

Given that resources are opaque i32 handles, the same could be true for implementing exported methods via //go:wasmexport.

@johanbrandhorst
Copy link
Member Author

Thanks for putting this together—this is exciting.

Generating a module that can act as a reactor and a command sounds like a great idea. I noticed this might conflict with how Node interprets a module. If a module exports both _start and _initialize, it will throw an exception: https://nodejs.org/api/wasi.html

1. One could argue this is undesired behavior, and Node could change.

2. What happens if a host detects and calls both _initialize and _start?

3. Exporting one or the other, but not both, implies some kind of configuration or detection.
   
   * In this TinyGo PR I experimented with detecting lack of main.main as the trigger for "reactor" mode with _initialize as the entry point: [runtime, builder: WebAssembly reactor mode tinygo-org/tinygo#4082](https://github.com/tinygo-org/tinygo/pull/4082)

Thank you for the information about Node's behavior here, I wasn't aware. That is certainly troubling. I will try to see what if any other precedent there is for this behavior in the ecosystem to see whether we or Node are in the wrong.

If a host calls both _initialize and _start, it will run initialization once (initialization has to be protected with something like a sync.Once to be idempotent) and then run func main(). Just calling _start will accomplish the same thing.

Indeed, if we do need some way to allow users to choose whether to build a command (executing func main()) or library (just initializating and exporting functions), this proposal would need to add some way for users to turn that knob. I don't want to prejudice that discussion until we know if we need it.

@johanbrandhorst
Copy link
Member Author

Given that resources are opaque i32 handles, the same could be true for implementing exported methods via //go:wasmexport.

This may be true, but I still think this proposal serves as an MVP that we can enhance with method support in a subsequent proposal once the initial hurdles have been overcome.

@ydnar
Copy link

ydnar commented Jan 22, 2024

Thank you for the information about Node's behavior here, I wasn't aware. That is certainly troubling. I will try to see what if any other precedent there is for this behavior in the ecosystem to see whether we or Node are in the wrong.

If a host calls both _initialize and _start, it will run initialization once (initialization has to be protected with something like a sync.Once to be idempotent) and then run func main(). Just calling _start will accomplish the same thing.

Maybe it’s a bigger question about what is defined behavior. Is having both _initialize and _start valid, or undefined? Having only one entry point is less ambiguous, e.g. the host can only call one, but not both (or choose, which could be contrary to the user’s expectation).

@ydnar
Copy link

ydnar commented Jan 22, 2024

Indeed, if we do need some way to allow users to choose whether to build a command (executing func main()) or library (just initializating and exporting functions), this proposal would need to add some way for users to turn that knob. I don't want to prejudice that discussion until we know if we need it.

Have had previous discussions about -buildmode=wasm-reactor to mirror -buildmode=c-shared.

@johanbrandhorst
Copy link
Member Author

I created an issue to ask the NodeJS devs for the source of this design decision: nodejs/node#51544

@cjihrig
Copy link

cjihrig commented Jan 22, 2024

Hey. Node developer that implemented that design decision here. 👋

That change was nearly four years ago, and I have since forgotten the exact motivation. However, I was able to dig this up: WebAssembly/WASI@d8b286c. At that point in time, WASI commands had a _start() function, and WASI reactors had an _initialize() function. Commands and reactors were mutually exclusive.

WASI has changed a good bit since then. I no longer work on WASI, so I don't know if that design decision is still valid or not. I would recommend checking with the folks in the WASI repos.

@zetaab
Copy link

zetaab commented Jan 22, 2024

WebAssembly/wasi-http#95 contains discussion to use _initialize func. So if that is not possible to golang, it would be difficult

@johanbrandhorst
Copy link
Member Author

Any user created func init() would be run in _initialize, is this not sufficient?

@johanbrandhorst
Copy link
Member Author

Hey. Node developer that implemented that design decision here. 👋

That change was nearly four years ago, and I have since forgotten the exact motivation. However, I was able to dig this up: WebAssembly/WASI@d8b286c. At that point in time, WASI commands had a _start() function, and WASI reactors had an _initialize() function. Commands and reactors were mutually exclusive.

WASI has changed a good bit since then. I no longer work on WASI, so I don't know if that design decision is still valid or not. I would recommend checking with the folks in the WASI repos.

Thanks so much for providing your input and this reference. It seems this doc now lives at https://github.com/WebAssembly/WASI/blob/a7be582112b35e281058f1df7d8628bb30a69c3f/legacy/application-abi.md. I wonder, given that this is now under the legacy heading, whether this statement is still true:

These kinds are mutually exclusive; implementations should report an error if asked to instantiate a module containing exports which declare it to be of multiple kinds.

If so, this design would need to change to allow the user to choose whether to compile a Command or a Library (Reactor). @sunfishcode perhaps you could provide some guidance here?

@sunfishcode
Copy link

sunfishcode commented Jan 23, 2024

The _start and _initialize functions and legacy/application-abi.md file are all Preview 1 things. Many Preview 1 Wasm engines recognize _start for commands, and some recognize _initialize as an entrypoint for reactors.

Preview 2 is based on the Wasm component model.

  • For the command world, there is an exported run function which is the command entrypoint (analogous to what _start was in Preview 1).
  • There isn't an export for reactor-style wasm programs. The component model itself has a mechanism to call functions on initialization. Unlike core-Wasm's start section, the component-model's start section doesn't need to worry about memory not being exported yet, so we can use for arbitrary initialization (analogous to what _initialize was in Preview 1).
    • If the tooling you use to go from a core-wasm module to a component supports it, the core-wasm _initialize function may be automatically wired up to the component-model start section.

Edit: I was mistaken about the component-model start function. It's not permitted to call imports, so it's not usable for arbitrary initialization code. There are ongoing discussions about this.

@johanbrandhorst
Copy link
Member Author

Thanks for the explanation. This proposal targets our existing wasm implementations, js/wasm and wasip1/wasm. We'll have a think about the best way to go about this that doesn't paint us into a corner when it comes to adding support for wasip2 down the line.

@ydnar
Copy link

ydnar commented Jan 23, 2024

  • If the tooling you use to go from a core-wasm module to a component supports it, the core-wasm _initialize function may be automatically wired up to the component-model start section.

What’s an example of tooling that converts a module to a component that supports the component model start section?

Wasmtime seems to not support the start section? https://github.com/bytecodealliance/wasmtime/blob/e9d580776ee27f4ed59ba334765aacbcc22fa6e4/crates/environ/src/component/translate.rs#L623

@johanbrandhorst
Copy link
Member Author

johanbrandhorst commented Jan 24, 2024

In light of the discussion around NodeJS's behavior and the documented separation between _initialize and _start in wasip1, we've updated the proposal to include a new -buildmode=wasip1-reactor, used to instruct the compiler to produce a Wasm binary with an _initialize function in place of the _start function. The use of go:wasmexport is limited to this new build mode, which is only available for GOOS=wasip1.

@cherrymui
Copy link
Member

Thanks for the proposal! Looks good overall.

-buildmode=wasip1-reactor

Is there something similar for js/wasm? Or the library/export mechanism is very different? Also, will the mechanism be similar for later wasip2, or eventual wasi? If so, maybe we can choose a more general name like wasm-library, so we don't need to have a different build mode for each of them? (For start it is okay to only implement on wasip1, just like the c-shared build mode is not implemented on all platforms.)

_initialize

Is _initialize required to be called before any exported functions can be called? Or, the first time it calls into Go _initialize is called if not already? Or the Wasm execution engine always automatically calls _initialize on module load time, so it is guaranteed to be called first?

In the absence of threading or stack switching capability in Wasm, the simplest option is to document that all goroutines still running when the invocation of the go:wasmexport function returns will be paused until the control flow re-enters the Go application.

So, this sounds like that at the end of the exported function, the Go runtime will not try to schedule other goroutines to run but directly return to Wasm? I assume this might be okay. But js.FuncOf seems to choose a different approach. This is also related to the discussion in #42372. Could you explain the reason for choosing this approach?

GODEBUG=wasmgoroutinemon=1

I'm not sure we want this debug mode. As you mentioned, it is probably not uncommon to have background goroutines. If one wants to ensure there is no goroutine at the time of exported function exiting, one probably can check it with runtime.NumGoroutine.

Thanks.

@johanbrandhorst
Copy link
Member Author

-buildmode=wasip1-reactor

Is there something similar for js/wasm? Or the library/export mechanism is very different? Also, will the mechanism be similar for later wasip2, or eventual wasi? If so, maybe we can choose a more general name like wasm-library, so we don't need to have a different build mode for each of them? (For start it is okay to only implement on wasip1, just like the c-shared build mode is not implemented on all platforms.)

Any wasm module can declare exports, but we don't anticipate that exporting methods like this is generally useful to users of js/wasm - we have js.FuncOf today to make Go code callable from JS, and making it callable from Wasm doesn't seem nearly as useful for that platform.

For wasip2, as illustrated by Dan's reply above, it's not clear what the export mechanism would look like yet. The name wasip1-reactor is chosen to be deliberately specific to wasip1. The exact functionality in this proposal would be limited to wasip1 forever, and any hypothetical wasip2 proposal would likely have to explain how/if wasmexport will be available for that target initially.

_initialize

Is _initialize required to be called before any exported functions can be called? Or, the first time it calls into Go _initialize is called if not already? Or the Wasm execution engine always automatically calls _initialize on module load time, so it is guaranteed to be called first?

The expectation within the greater wasip1 ecosystem seems to be that if _initialize is exported by a module, it will be called before any exported methods are called. Our implementation wouldn't automatically call _initialize if it hasn't been called, it would likely just crash horribly.

In the absence of threading or stack switching capability in Wasm, the simplest option is to document that all goroutines still running when the invocation of the go:wasmexport function returns will be paused until the control flow re-enters the Go application.

So, this sounds like that at the end of the exported function, the Go runtime will not try to schedule other goroutines to run but directly return to Wasm? I assume this might be okay. But js.FuncOf seems to choose a different approach. This is also related to the discussion in #42372. Could you explain the reason for choosing this approach?

Yes, once the exported function returns, we would not schedule other available goroutines but return to the host. The reason for this is that we believe it's what users would expect to happen, since the runtime and various standard libraries maintain their own goroutines that would make it hard to predict the behavior and runtime of exported functions. If you believe that to be an incorrect assumption we're happy to reconsider this. Note that this also includes goroutines started by the exported function itself.

GODEBUG=wasmgoroutinemon=1

I'm not sure we want this debug mode. As you mentioned, it is probably not uncommon to have background goroutines. If one wants to ensure there is no goroutine at the time of exported function exiting, one probably can check it with runtime.NumGoroutine.

This is a fair point, and we could certainly slim down the proposal by removing this and consider it as a future addition. Thanks!

@cherrymui
Copy link
Member

Sounds good, thanks.

I guess it might be fine to return to the host when the exported function returns. I guess one question is when the "background" goroutines run. If the exported functions get called and return, but none of them explicitly wait for the background goroutines, the background goroutines will probably never run? Would that be a problem for, say, timers?

@johanbrandhorst
Copy link
Member Author

The background goroutines could run again if the exported function gets called again. I think ideally users who want concurrent work in exported functions would utilize something like a sync.WaitGroup to ensure work is completed during the execution of the function. A future proposal might be able to tackle this by exposing something like _gosched to run all goroutines until asleep, but this proposal does not account for such a feature. Also, since Threads is stable in Wasm, we may be able to just spawn new threads in the near future, which could execute in parallel to the exported function.

@achille-roussel
Copy link
Contributor

The problem of having goroutines blocked after the export call returned isn't much different from what happens when invoking an import. When a WebAssembly module calls a host import, it yields control to the WebAssembly runtime; no goroutines can execute during that time.

The issue is amplified with exports because the WebAssembly runtime could keep the module paused for extended periods of time, and the expectation is that imports usually return shortly after they were invoked, but it isn't fundamentally different.

Despite the limitations, we can still deliver incremental value to Go developers by allowing them to declare exports.

@inliquid
Copy link

@johanbrandhorst when it comes to background goroutines, do you know if the proposed solution different from tinygo which supports exported functions?

@ydnar
Copy link

ydnar commented Jan 30, 2024

@johanbrandhorst when it comes to background goroutines, do you know if the proposed solution different from tinygo which supports exported functions?

We have a separate PR to TinyGo that prototypes the same model, suspending and resuming goroutines on an export call.

@cherrymui
Copy link
Member

The background goroutines could run again if the exported function gets called again.

If the exported function (or another exported function) gets called again, and that function returns without explicitly synchronizing or rescheduling, the background goroutine may still not run? I think blocking for a little while is not a problem, but it might be a problem if it never get to run (while the exported function get called again and again)?

As you mentioned, once we have thread supports, it may not be a problem.

@johanbrandhorst
Copy link
Member Author

It's true that goroutines may never get to run if there's no point in the exported function to yield to the runtime. I think that's still what I would expect to happen if I wrote my exported function this way. All alternatives would be more confusing I think (waiting before returning or maybe running the scheduler before executing the exported function).

As you say, we can hopefully improve this with threads support in the future.

@cherrymui
Copy link
Member

Okay. This is probably fine. We can change it later if there is any problem. Thanks.

@johanbrandhorst
Copy link
Member Author

I've removed the GODEBUG option, we can add that as an enhancement later and suggest users use NumGoroutines() for their debugging needs for now.

@aykevl
Copy link

aykevl commented Aug 30, 2024

@johanbrandhorst thanks for the clarification! That confirms my assumptions (and simplifies the implementation in TinyGo).

@aykevl
Copy link

aykevl commented Aug 31, 2024

Another question: what's the reason for disallowing blocking operations in wasip1? I understand why they can't happen in JavaScript, but as far as I can see wasip1 can support it (for example, an exported function could call time.Sleep which then calls out to a wasip1 function that blocks). Is it just for consistency, or something internal to the Go runtime?

(Calling time.Sleep inside a //go:wasmexport function would be fine in TinyGo on wasip1 for example).

@achille-roussel
Copy link
Contributor

If we were calling a blocking host function there would be no opportunity for the runtime to schedule other goroutines during that time (because we only have one thread).

For example, time.Sleep is supposed to block the current goroutine only, not the entire application, so it has to be implemented by the Go runtime and the only time we ever want to block on the host is when we are waiting for I/O events in the call to poll_oneoff.

@aykevl
Copy link

aykevl commented Sep 1, 2024

Right, that's different from how we do it in TinyGo. Once all available goroutines are suspended using time.Sleep, it sleeps using poll_oneoff until the first one is ready and runs it at that time (at least under wasip1, JS is different). I can modify the behavior so that it will panic instead of sleeping for compatibility.

@mattjohnsonpint
Copy link

mattjohnsonpint commented Sep 1, 2024

Wait a sec, are you saying any call to time.Sleep in a wasi app will panic? That seems problematic, especially if any imported libraries are using it.

aykevl added a commit to tinygo-org/tinygo that referenced this issue Sep 1, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1, but it is certainly
possible to extend it to other targets like GOOS=js, wasm-unknown, or
wasip2. It is also currently limited to -buildmode=c-shared, this is a
limitation that could easily be lifted in the future.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Sep 1, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1, but it is certainly
possible to extend it to other targets like GOOS=js, wasm-unknown, or
wasip2. It is also currently limited to -buildmode=c-shared, this is a
limitation that could easily be lifted in the future.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Sep 1, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1, but it is certainly
possible to extend it to other targets like GOOS=js, wasm-unknown, or
wasip2. It is also currently limited to -buildmode=c-shared, this is a
limitation that could easily be lifted in the future.
@inliquid
Copy link

inliquid commented Sep 2, 2024

@aykevl I think it's not necessary for TinyGo to 100% repeat what big Go implements. TinyGo is used widely by community because it's in many ways superior to big Go in Wasm world. And if you were to implement Wasm/WASI plugins TinyGo is the only option. WebAssembly support is still experimental and there in no guarantee that it won't be abandoned at some point, like it happened to go mobile.

@cherrymui
Copy link
Member

cherrymui commented Sep 2, 2024

Calling time.Sleep in a wasmexport function is just fine. It doesn't panic. E.g.

//go:wasmexport E
func E() {
	fmt.Println(time.Now())
	time.Sleep(1 * time.Second)
	fmt.Println(time.Now())
}

prints

2024-09-02 23:28:27.178171 +0000 UTC m=+0.000094126
2024-09-02 23:28:28.179793 +0000 UTC m=+1.001718085

It just sleeps a second.

And the Go runtime will naturally schedule other goroutines during time.Sleep. E.g.

//go:wasmexport E
func E() {
	fmt.Println(time.Now())
	go println("do something while sleeping")
	time.Sleep(1 * time.Second)
	fmt.Println(time.Now())
}

prints

2024-09-02 23:50:20.213754 +0000 UTC m=+0.000094501
do something while sleeping
2024-09-02 23:50:21.215162 +0000 UTC m=+1.001505626

In general, blocking syscalls are okay. It just blocks until the operation is done. It cannot return to the host, as the Wasm module itself is single threaded. However, the "syscalls" are provided by the host, so it calls to the host for the syscall implementation, which doesn't necessarily have to block. E.g. if I run the first wasmexport function above with wazero with configuration

	wazero.NewModuleConfig().
		WithStdout(os.Stdout).WithStderr(os.Stderr).
		WithNanosleep(func(ns int64){ go println("do something in host while sleeping"); time.Sleep(time.Duration(ns)) }).
		WithSysNanotime().
		WithSysWalltime()

(note the WithNanosleep line), it prints

2024-09-02 23:41:05.43752 +0000 UTC m=+0.000137335
do something in host while sleeping
2024-09-02 23:41:06.439137 +0000 UTC m=+1.001757335

It is only and indeed problematic if the wasmexport function blocks indefinitely, e.g. a deadlock, which will cause a runtime fatal error.

@cherrymui
Copy link
Member

cherrymui commented Sep 3, 2024

Right, that's different from how we do it in TinyGo. Once all available goroutines are suspended using time.Sleep, it sleeps using poll_oneoff until the first one is ready

@akavel I think this is similar to the implementation in this repo. At time.Sleep, the runtime will schedule other runnable goroutines to run. It calls the system sleep when there is no runnable goroutines. And wasmexport should not change that.

@aykevl
Copy link

aykevl commented Sep 3, 2024

@cherrymui Thank you for explaining! Yes that makes much more sense. I'll update the TinyGo PR to match.

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/611315 mentions this issue: cmd/compile: correct wasmexport result type checking

caarlos0 pushed a commit to goreleaser/goreleaser that referenced this issue Sep 10, 2024
This commit fixes the automatic extension when building the wasip1_wasm
target.

Additionally, in future Go versions, support will be added for
generating c-shared WASM binaries.
golang/go#65199


Therefore, this PR corrects the extension in the build process and
removes the .h file from the release when c-shared is enabled and the
target is WASM.
gopherbot pushed a commit that referenced this issue Sep 11, 2024
The function resultsToWasmFields was originally for only
wasmimport. I adopted it for wasmexport as well, but forgot to
update a few places that were wasmimport-specific. This leads to
compiler panic if an invalid result type is passed, and also
unsafe.Pointer not actually supported. This CL fixes it.

Updates #65199.

Change-Id: I9bbd7154b70422504994840ff541c39ee596ee8f
Reviewed-on: https://go-review.googlesource.com/c/go/+/611315
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Achille Roussel <achille.roussel@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 2, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1, but it is certainly
possible to extend it to other targets like GOOS=js, wasm-unknown, or
wasip2. It is also currently limited to -buildmode=c-shared, this is a
limitation that could easily be lifted in the future.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1, but it is certainly
possible to extend it to other targets like GOOS=js, wasm-unknown, or
wasip2.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1, but it is certainly
possible to extend it to other targets like GOOS=js, wasm-unknown, or
wasip2.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
aykevl added a commit to tinygo-org/tinygo that referenced this issue Oct 3, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
ydnar pushed a commit to tinygo-org/tinygo that referenced this issue Oct 4, 2024
This adds support for the `//go:wasmexport` pragma as proposed here:
golang/go#65199

It is currently implemented only for wasip1 and wasm-unknown, but it is
certainly possible to extend it to other targets like GOOS=js and
wasip2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Accepted
Development

No branches or pull requests