golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.71k stars 17.62k forks source link

cmd/compile: replace CallImport with go:wasmimport directive #38248

Closed neelance closed 1 year ago

neelance commented 4 years ago

The wasm architecture currently uses a special CallImport assembler instruction to call function imports. This approach is quite inflexible and only compatible with the ABI used by wasm_exec.js.

The WebAssembly ecosystem is converging on using WASI (WebAssembly System Interface) as a standardized interface between the WebAssembly binary and its host (similar to system calls). I am working on adding support for WASI to Go. As a first step, Go needs to be able to use function imports with the ABI used by WASI.

For this reason I propose to replace CallImport with a new compiler directive go:wasmimport. It can be used like this:

//go:wasmimport __wasi_proc_exit wasi_unstable proc_exit
func __wasi_proc_exit(code int32)

By default go:wasmimport will use the ABI used by WASI. An optional parameter can be used for backwards compatibility with wasm_exec.js. This parameter is called abi0 because wasm_exec.js is reading the parameters form memory with Go's normal ABI0 layout:

//go:wasmimport wasmExit go runtime.wasmExit abi0
func wasmExit(code int32)

I do not plan to add other ABIs. On the contrary, the abi0 mode is supposed to go away once WASI can be used as the primary interface (it is still missing certain features).

The go:wasmimport directive will not be covered by Go's compatibility promise as long as the wasm architecture itself is not considered stable.

rsc commented 4 years ago

I'm confused about there being two different ABIs and also about what is optional and not. It would be nice if the optional fields were last, not in the middle of the list. Then you can tell what things mean by looking at just the argument count - you don't have to know that "go" is apparently a magic word and that when it is arg[2] it is not the real arg[2].

Could you please write the suggested docs describing the syntax in full? And maybe rotate the optional words to the end? It would be even better if there were not two different optional ABI words. Can it be reduced to one?

neelance commented 4 years ago

Right, I forgot to include a clear explanation of the arguments of the directive. The usage is like this:

//go:wasmimport localname importmodule importname [abi0]

localname is the name of the Go function, same as with //go:linkname.

importmodule and importname identify the function to import. WebAssembly functions always live within a module. For WASI this module is currently called wasi_unstable, see here. For wasm_exec.js it is called go, see https://github.com/golang/go/blob/aa3413cd98b6e11fe0d37d3d2a489a9cd83b47ad/misc/wasm/wasm_exec.js#L261-L262.

[abi0] is the optional flag to indicate that this import is using the ABI0 way for passing arguments.

Hope this helps.

rsc commented 4 years ago

//go:linkname is kind of the odd one out for compiler directives (see go doc compile) in that it doesn't just implicitly apply to the next function declaration. Instead of adding another like that, it would probably be better to attach it to a function implicitly, like the others (example: //go:noinline).

Then instead of:

//go:wasmimport wasmExit go runtime.wasmExit abi0
func wasmExit(code int32)

you'd have

//go:wasmimport go runtime.wasmExit abi0
func wasmExit(code int32)

Is there always a function declaration to attach to?

The syntax would be down to:

//go:wasmimport importmodule importname [abi0]

I'm skeptical about exposing the name abi0. If the default is likely to be Go code, maybe use [wasi] as the optional mode? Or maybe if the importmodule is go, assume the Go ABI? It still feels like there is more simplification possible here.

neelance commented 4 years ago

I fully agree that a version without localname would be better. In fact this was my initial plan.

However, all the current directives that apply to the next function are just flags without any parameters. They get processed by the pragmaValue function in lex.go and the return type is type Pragma uint16. No arguments possible.

This is why I opted for not doing a large change to Go's lexer/parser and instead copied a solution that already exists: //go:linkname.

About the abi flag: The default should be the abi that wasi uses, since it is a more native way to pass arguments to a WebAssembly function. The abi0 flag only needs to exist so I don't have to change everything in a massive CL. This way I can still support the abi that wasm_exec.js uses and slowly migrate to wasi. Having a special case for the importmodule go is possible, sure, but such implicit behavior change seems bad to me.

rsc commented 4 years ago

However, all the current directives that apply to the next function are just flags without any parameters. They get processed by the pragmaValue function in lex.go and the return type is type Pragma uint16. No arguments possible.

That was true a couple weeks ago but is no longer true (I just changed that, in preparation for some other possible //go: directives). So let's make the proposal what we both want, that it applies to the next function. I'm happy to work with you off-issue if you need help making that work.

So now we're just down to the ABI flag. The problem with abi0 is that we have not yet exposed that name and don't really plan to. I would rather refer to it as the go ABI. And it seems kind of redundant to have to say that the go importmodule uses the go ABI. Will other importmodules use that same ABI?

I'm also a little confused about "The abi0 flag only needs to exist so I don't have to change everything in a massive CL." since the //go:wasmimport directive doesn't exist today.

@neelance is it the case that long term nothing will say abi0? That is, will it be that some future Go release removes support for the abi0 annotation?

/cc @aclements how do you feel about exposing the name abi0 here?

neelance commented 4 years ago

So let's make the proposal what we both want, that it applies to the next function.

Great!

Will other importmodules use that same ABI?

They should not.

is it the case that long term nothing will say abi0? That is, will it be that some future Go release removes support for the abi0 annotation?

Yes, that's the long term plan, but I can't say when this will be the case. It depends a lot on how fast the development of WASI goes until it has enough features to completely replace the custom interface that wasm_exec.js currently uses.

I don't mind picking a different flag name than abi0. Making it implicit with the name of the importmodule feels a bit bad to me, but I do not strongly oppose it either.

aclements commented 4 years ago

I'm actually fine with exposing the name abi0. The intent of calling them ABI0 and ABIInternal was that we could expose the name "ABI0" if we ever froze another, faster ABI as ABI1. I'm not sure if we'd want to spell it "abi0" or "ABI0" given that it is an initialism.

However, it actually seems more reasonable to me to assume everything in the "go" module follows the "Go ABI" and omit the ABI from the directive. If I understand correctly, this is effectively a "closed" module for wasm_exec.js's exports and users can't define new abi0 interfaces in either this module or any other module, so the set of things that use abi0 is fixed and bounded and exactly those in the "go" module.

neelance commented 4 years ago

All right, I agree that it is good to discourage anyone else from using abi0 by exposing it less. Shall I edit the proposal in the first post or add a new version below?

rsc commented 4 years ago

@neelance, please add a new comment below, so it's easier to see the history. Thanks.

neelance commented 4 years ago

Here is the new full proposal text:


The wasm architecture currently uses a special CallImport assembler instruction to call function imports. This approach is quite inflexible and only compatible with the ABI used by wasm_exec.js.

The WebAssembly ecosystem is converging on using WASI (WebAssembly System Interface) as a standardized interface between the WebAssembly binary and its host (similar to system calls). I am working on adding support for WASI to Go. As a first step, Go needs to be able to use function imports with the ABI used by WASI.

For this reason I propose to replace CallImport with a new compiler directive go:wasmimport. It can be used like this:

//go:wasmimport importmodule importname

Concrete example:

//go:wasmimport wasi_unstable proc_exit
func __wasi_proc_exit(code int32)

importmodule and importname identify the function to import. WebAssembly functions always live within a module. For WASI this module is currently called wasi_unstable, see here. For wasm_exec.js it is called go, see https://github.com/golang/go/blob/aa3413cd98b6e11fe0d37d3d2a489a9cd83b47ad/misc/wasm/wasm_exec.js#L261-L262

By default go:wasmimport will use the ABI used by WASI. There is a special case for backwards compatibility with wasm_exec.js: If importmodule is go, then the arguments will get passed in a way so wasm_exec.js can read them from memory with Go's ABI0 layout. This special case will be removed once the interface of wasm_exec.js has been fully superseded by WASI (currently it is still missing certain features).

The go:wasmimport directive will not be covered by Go's compatibility promise as long as the wasm architecture itself is not considered stable.

aykevl commented 4 years ago

From the TinyGo side, I fully support this proposal. Right now we use something like this (following your example):

//go:wasm-module wasi_unstable
//export proc_exit
func __wasi_proc_exit(code int32)

It works as a stopgap, but if this proposal is accepted I'll make sure TinyGo will switch over to the new format.

I have one comment:

By default go:wasmimport will use the ABI used by WASI.

What is this ABI? Is it specified somewhere? It seems to me that it's really just the C ABI and if so, I think it should be specified as such. Some related discussions:

neelance commented 4 years ago

On the lowest level the ABI consists of passing data via arguments (on the WebAssembly stack) to WebAssembly's call instruction. There may be higher level additions in the future, such as a standardized way of how to pass strings, but currently this seems to be still in development. So by "ABI used by WASI" I really mean whatever WASI is doing right now and in the future.

aykevl commented 4 years ago

On the lowest level the ABI consists of passing data via arguments (on the WebAssembly stack) to WebAssembly's call instruction.

Nitpick: ABI is much more than a calling convention! It also involves struct layout, alignment, etc. (relevant when passing pointers around).

But in general, because you can use this facility for more than just WASI (e.g. -buildmode=c-shared) I think it's important to specify the things that WASI doesn't specify. For example, the WASI ABI passes all structs by reference, sidestepping the issue whether small structs may be passed directly in (virtual) registers instead of on the stack. Go will have to pick a side here. Therefore I would suggest investigating which ABI is used by Clang or Rust and following that.

neelance commented 4 years ago

My goal with go:wasmimport is to support WASI and it will do whatever WASI needs now or in the future. I think this is very reasonable, because WASI will set certain standards in the WebAssembly ecosystem on how to pass data between WebAssembly modules. This may include other aspects than the calling convention.

I do not want to define some "WebAssembly interface for Go" here, since I believe this is too early and Go should rather follow the WebAssembly ecosystem (namely WASI) than to diverge with its own interface. So if there is some concern about other people using this interface, then I would be okay with restricting go:wasmimport to the runtime and syscall/js packages. If people really want to experiment in other contexts, then they would need to patch the Go compiler first.

rsc commented 4 years ago

Everyone seems in favor of what we've converged on. Thanks for the good discussion.

Based on the discussion, this seems like a likely accept.

cherrymui commented 4 years ago

Sorry about being late.

Does go:wasmimport work for arbitrary modules and functions, or only for a list of special functions?

A bit bikeshedding: is "go" the best name for wasm_exec.js? From the Go code side, everything is go except the imported functions. It is true that the module is currently named "go", but we can change that. Maybe "jsgo" or "gojs"?

neelance commented 4 years ago

Does go:wasmimport work for arbitrary modules and functions, or only for a list of special functions?

There is no list. The parameter types need to be supported. Like I said above, I would be okay with restricting it to runtime and syscall/js if people are worried about usage outside of the Go project itself.

A bit bikeshedding: is "go" the best name for wasm_exec.js?

I am fine with changing this name. No preference on my end.

rsc commented 4 years ago

Replying to the previous two comments:

Leaving as likely accept for another week but we seem to have converged.

cherrymui commented 4 years ago

Thanks. I also think it is reasonable to start with restricting it to runtime and syscall/js. We can make it more general later if necessary.

rsc commented 4 years ago

No change in consensus, so accepted.

gopherbot commented 4 years ago

Change https://golang.org/cl/243677 mentions this issue: [dev.link] implement wasmimport directive

gopherbot commented 4 years ago

Change https://golang.org/cl/248739 mentions this issue: [dev.link] implement wasmimport directive

gopherbot commented 4 years ago

Change https://golang.org/cl/252828 mentions this issue: [dev.link] implement wasmimport directive

slinkydeveloper commented 3 years ago

I wonder, can we do the same for exports? Starting from the patch https://golang.org/cl/252828, it should be quite trivial to implement a go:wasmexport directive like:

//go:wasmexport myExportedFunc
func myExportedFunc() {}

This might be a first step towards https://github.com/golang/go/issues/25612 and https://github.com/golang/go/issues/41715.

I can take care of filing a PR for it, I already have a prototype on my machine

neelance commented 3 years ago

@slinkydeveloper As you can see in this issue, the addition of //go:wasmimport was discussed as a proposal first. You should probably file a proposal for your //go:wasmexport, too.

slinkydeveloper commented 3 years ago

@neelance I think https://github.com/golang/go/issues/25612 pretty much cover it, but sure i can open another issue for that

neelance commented 3 years ago

25612 is not a proper proposal, since there are a lot of aspects of the intended behavior that are not clear. I see some difficulties around //go:wasmexport and goroutines, so I'm looking forward to a discussion on how we could solve these. The proper place for such a discussion is a proposal.

slinkydeveloper commented 3 years ago

@neelance then fill https://github.com/golang/go/issues/42372 with your questions, i'm looking forward to address them.

gopherbot commented 3 years ago

Change https://golang.org/cl/290112 mentions this issue: all: implement wasmimport directive

vedantroy commented 3 years ago

@rsc @cherrymui How do the new Go ABI changes (not sure if that's the right term) affect this proposal? I had a working version of this proposal here before the ABI changes, but it looks there's been significant changes in the past few months.

cherrymui commented 3 years ago

I would imagine the ABI changes themselves don't interfere much with Wasm imports. However, the compiler has gone a significant refactoring (e.g. the gc package is not split into multiple packages), so you'll need to rebase and update it to the new compiler structure. Thanks.

vedantroy commented 3 years ago

@cherrymui Got it, will rebase. The reason I asked was b/c the wasmimports proposal does generate "wrapper functions" that convert between the Go calling conv & the Web Assembly calling conv, so I assumed that ABI wrapper stuff/multiple ABIs might be relevant.

cherrymui commented 3 years ago

@vedantroy thanks for the information. Currently the ABI wrappers are only enabled on AMD64. When we get to it on Wasm, we might need to take wasm import wrapper into consideration (or not, the two kinds of wrappers may well work together).

gopherbot commented 3 years ago

Change https://golang.org/cl/350737 mentions this issue: all: implement wasmimport directive

decibelcooper commented 3 years ago

FYI I submitted a working rebased version of @vedantroy and @neelance's change here: https://go-review.googlesource.com/c/go/+/350737

I am personally interested in seeing their work towards WASI support come to fruition, so please let me know how else I can help.

backkem commented 2 years ago

Thanks. I also think it is reasonable to start with restricting it to runtime and syscall/js. We can make it more general later if necessary.

I'd like to argue for making this directive open. Usage of the WASI ABI seems like a fair assumption. However, targeting only standardized WASI host interfaces is too narrow in my mind. There is a wealth of different host interfaces being created for WebAssembly. To give some examples: runtimes for embedded, serverless functions, edge computing, smart contracts, hosts with access to accelerators for neural nets and cryptography and bespoke plug-in systems (ref1, ref2, ref3). Some of these cases may result in standards but many will be maintained by their own community/company. Likewise many WASI standardized host interface modules (E.g.: wasi-nn) will likely be irrelevant to the Go standard library. In addition, opening this up allows more work for topics like WASI support (#31105) to happen out of the main tree. Once it stabilizes it can be moved over to serve as the foundation for a GOOS=WASI.

Allowing the go:wasmimport directive in any code in combination with a barebones GOOS=none (assumes the WASI ABI as default) would give the community the freedom to build guests for all the different host interfaces described above. Maybe this directive can be explicitly marked as experimental for a couple releases until it is deemed mature.

backkem commented 2 years ago

To clarify, do agree that usage of the WASI ABI seems like a fair assumption. However, we should treat the ABI and functions imports separately. From WASI Design Principles:

One of the great things about WebAssembly is that there's no syscall instruction, so "syscalls" in WebAssembly are just calls to imported functions, which could be native functions provided by the runtime, or could be other WebAssembly modules.

Importing functions is how WASM modules interact with each-other and their host. This is inspired by JavaScript modules interacting with each-other and the browser. Opening up the go:wasmimport directive is a step towards supporting WASM modules. A GOOS=none option would allow the module developer to pick and choose which external functions (or modules) he imports for his use-case.

inkeliz commented 2 years ago

I would be okay with restricting go:wasmimport to the runtime and syscall/js packages.

I don't think that is right. The syscall/js is too slow and have a lot of overhead, allocation and put more pressure on the GC. Also, the syscall/js says: "Its current scope is only to allow tests to run, but not yet to provide a comprehensive API for users.", but how could we build another package without the CallImport (or without the new //go:wasmimport)? We already need to use syscall/js due to the lack of export.

Removing the CallImport (or making it exclusive to syscall/js and runtime) means that we must use the slowest possible route: syscall/js.


Just to make things clear, currently, on GioUI: the same code using syscall/js is almost 1.6x slower compared with CallImport. That issue is already known (https://github.com/golang/go/issues/32591). I'm comparing the code that does the exactly: interact with WebGL and execute the same functions. Even after "optimizing" the syscall/js by using Invoke and Bind (to avoid string) and also uses unsafe.Pointer(&slice[0]) and creates a new Uint8Array(go._inst.exports.mem.buffer, dataPtr, dataLen), it still 1.6x slower. Without such optimization it's almost 2x slower. That is from real use-case scenario.

Anyway, everybody knows that the syscall/js is slow, and I understand the reason. However, removing the CallImport (or //go:wasmimport) doesn't make any sense to me: we should consider that it is only way to not use the syscall/js and get better performance. Most of the time is spent on makeArgs and also with the CopyBytes. However, using the CallImport we can call things "directly", bypassing the makeArgs and also without copying bytes, instead it can create a Array Viewer directly on the JS side. That is way faster than using syscall/js, almost 1.6 times faster in one real case.

Requiring to patch Golang itself to use it is quite a mess, that is not easy and also need to install or keep updated a custom modified version, just to use a feature that we already have today.

vedantroy commented 2 years ago

@inkeliz I believe the restriction is just to test out the new feature, and in the long run //go:wasmimport would be allowed anywhere

inkeliz commented 2 years ago

@vedantroy what will be the alternative in the meanwhile? Since CallImport is also removed when //go:wasmimport is introduced.

johanbrandhorst commented 1 year ago

FYI I submitted a working rebased version of @vedantroy and @neelance's change here: https://go-review.googlesource.com/c/go/+/350737

I am personally interested in seeing their work towards WASI support come to fruition, so please let me know how else I can help.

Looks like this effort has stalled too, are you still interested in helping merge this? There is renewed interest in supporting WASI as it has become more established of a standard in the wasm ecosystem. I'd love to help bring this over the line but it seems like we might need some buy in from deeply knowledgable parties like @neelance and the Go compiler team (CC @cherrymui) to get this big change done. Sorry for the pings, but hoping to move this forward 😁.

johanbrandhorst commented 1 year ago

I intend to work with @neelance to hopefully bring this over the line when the tree opens in February.

james-lawrence commented 1 year ago

@johanbrandhorst @neelance I'm happy to also jump in and help via testing. I have a bunch of usecases around wasi. feel free to reach out if you want extra eyes on things or even as a black box user. (I could probably help at the compiler level too after a few rounds I just havent looked at the golang toolchain in awhile)

gedw99 commented 1 year ago

@johanbrandhorst @neelance I'm happy to also jump in and help via testing. I have a bunch of usecases around wasi. feel free to reach out if you want extra eyes on things or even as a black box user. (I could probably help at the compiler level too after a few rounds I just havent looked at the golang toolchain in awhile)

Just like @james-lawrence said I also have a bunch of use cases that I am happy to try out and also to test things.

Very encouraging to see the community keeping on towards making this a reality too.

gopherbot commented 1 year ago

Change https://go.dev/cl/463018 mentions this issue: all: implement wasmimport directive

mdempsky commented 1 year ago

Should a malformed //go:wasmimport directive be a compiler error even when GOARCH is not wasm? Or should it just be silently ignored by the compiler?

johanbrandhorst commented 1 year ago

I don't know if there's any precedent but I'd like to see it always error.

achille-roussel commented 1 year ago

Hello!

I am following up here on a topic we discussed live among the group working on this issue, and believe we need input to make the right decision.

The question that came up was how to deal with //go:wasmimport functions which accept pointers as arguments in relation with escape analysis?

The WebAssembly type system also only knows about integers and floats, it does not have the notion of pointers. Pointers are represented by 32 bits integers since the addressable space of WebAssemble is 4GiB.

However, the GC does not know that the underlying host is a WebAssembly host, and it assumes the //go:wasmimport functions (which do not have a body) may retain the pointers and therefore it places them on the heap.

We believe that the application can expect the host not to retain any pointers passed to those functions, since they cannot be tracked by its GC.

We considered adding //go:noescape to these function declarations, but since it is impossible for the runtime and application to negotiate the lifetime of pointers, it seems that these two statements should hold true:

Alternatively, we considered forbidding the use of pointer types in the signature of //go:wasmimport functions. However, this forces the use of unsafe to convert Go pointers to integers when calling these functions. It also likely requires the application code to think about objects lifetime and add the necessary calls to runtime.KeepAlive (given, there are no threads so the application cannot do GC while it's calling to the host, but it seems like fertile ground for breaking programs if/when WebAssembly gets threads?).

james-lawrence commented 1 year ago

I believe the two assumptions are true. is the usecase that the host function uses the pointer to know what offset to access in the wasm memory?

Alternatively, we considered forbidding the use of pointer types in the signature of //go:wasmimport functions

you can always relax such a restriction later once the problem space is better understood. so it might be worth assuming go:noescape and prohibiting pointers for the time being. get the rest of the work in place and then revisit once people are able to use and experiment with wasi.

achille-roussel commented 1 year ago

I believe the two assumptions are true. is the usecase that the host function uses the pointer to know what offset to access in the wasm memory?

Yes, in what I have experienced with pointers are passed so the host can read or write a buffer in the program memory (e.g. for I/O, or output parameters of syscalls like stat).