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

proposal: syscall/js: allow passing a slice of bytes directly to JavaScript #46473

Open
BenLubar opened this issue May 31, 2021 · 8 comments
Open

Comments

@BenLubar
Copy link

In the same way that you can pass a pointer to an element of a byte slice to a function via CGo without copying it, assuming the function you're calling doesn't hold on to the pointer after it returns, it should be possible to call JavaScript functions with a Uint8Array that points directly to Go memory.

This could be used to make calls to functions like WebGLRenderingContext.bufferData more efficient, as the data wouldn't need to be copied twice (once from Go to JavaScript and then again by the browser).

The same caveats as passing a *byte to CGo would apply: if the function you're calling holds on to the pointer after it returns, the pointer will probably eventually point to something else.

@BenLubar
Copy link
Author

BenLubar commented Jun 3, 2021

For data, here's a profile of a (relatively simple) game playing a recording showing about 3-4% of the total run time taken by the jsBytes function, which copies a []byte to a new Uint8Array, which is then used once and discarded.

https://share.firefox.dev/3idWNwD

@agnivade
Copy link
Contributor

agnivade commented Jun 4, 2021

Yes you can use unsafe tricks to pass a pointer and an offset, and then jump to JS, and take out that slice of memory from inst.exports.mem.buffer casted to a Uint8Array. All that works fine.

I guess the question is do we want to make such tricks part of the syscall package or let it be something outside the standard library. And moreover, if we do this, then we would now have two ways to pass bytes - one via copying and one without.

@neelance

@BenLubar
Copy link
Author

BenLubar commented Jun 4, 2021

Is there a way to get inst.exports.mem.buffer from Go code without modifying wasm_exec.js?

@BenLubar
Copy link
Author

BenLubar commented Jun 4, 2021

Looks like you can do this to get it:

var goValue js.Value
*(*uint64)(unsafe.Pointer(&goValue)) = 0x7FF8000100000006
_ = goValue.Get("mem").Get("buffer")

Should you? Probably not.

@agnivade
Copy link
Contributor

agnivade commented Jun 4, 2021

We don't need to modify inst.exports.mem.buffer from Go code. We can just pass the pointer and the length to JS, and read out the slice.

You can check this commit to see how it was done before the copy functions were introduced: https://github.com/agnivade/shimmer/commit/d08fb873f760d93922e97085a792539e714df4a9/

@atdiar
Copy link

atdiar commented Jun 4, 2021

I wish for it too but I'm wondering if it wouldn't require some kind of ownership transfer semantics to avoid the pitfalls of aliasing.
If so, that might be a bit more involving.

@BenLubar
Copy link
Author

BenLubar commented Jun 7, 2021

We don't need to modify inst.exports.mem.buffer from Go code. We can just pass the pointer and the length to JS, and read out the slice.

You still need to access inst.exports.mem.buffer in the JavaScript code, which either means modifying wasm_exec.js or passing js.Value{ref(6)} to the JavaScript

but at least it's possible without language/standard library changes

@ianlancetaylor ianlancetaylor added this to Incoming in Proposals (old) Jun 8, 2021
@inkeliz
Copy link

inkeliz commented Jun 18, 2021

What is the problem of change wasm_exec.js?


I think that the best option to do that is the following:

main_js.s

#include "go_asm.h"
#include "textflag.h"

TEXT myfunc(SB), NOSPLIT, $0
  CallImport
  RET

main.go

func main() {
       myfunc([]byte("your_byte_array"))
}

func myfunc(b []byte) // That will call the "assembly function"

wasm_exec.js (insert the following into wasm_exec):

(() => {
      Object.assign(go.importObject.go, {
           "main.myfunc": (sp) => {
              sp += 8

               const s = new Uint8Array(go._inst.exports.mem.buffer,
                      (go.mem.getUint32(sp + 0, true) + go.mem.getInt32(sp + 4, true) * 4294967296), 
                      (go.mem.getUint32(sp + 8, true) + go.mem.getInt32(sp + 12, true) * 4294967296),
              )
              // The `s` have the value of `b []byte`.
             // You can call anything here, without care about UTF-8 Decoding and the Reflect API.
           }
      },
})()

I didn't test the code above, but I used something very similar to call WebGL functions.


The best option is not to use syscall/js at all, for instance that is my attempt to use only CallImport for all WebGL functions. Before (using syscall/js) it takes ~8.0ms per frame. After replace to CallImport it drops to ~2.9ms. There's another version of that, a little bit slower but way faster than syscall/js. I'm trying to make some generator to create the CallImport automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Incoming
Development

No branches or pull requests

6 participants