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

Buffer support #13

Merged
merged 7 commits into from
Jan 6, 2023
Merged

Buffer support #13

merged 7 commits into from
Jan 6, 2023

Conversation

RReverser
Copy link
Contributor

I don't understand the test structure - they seem to be adapted versions of official N-API tests, but without GYP and with some other modifications. Are they converted automatically somehow or do you rewrite them manually?

Also, you might notice that I went the slightly easier path for napi_create_external_buffer by simply wrapping napi_create_external_arraybuffer. In theory, both should have more or less the same effect, except that array buffer might be reused for other views and so the finalize callback might be called a bit later; that said, finalize callbacks are already not guaranteed to run at specific time (or at all) so this shouldn't be an issue.

@RReverser RReverser force-pushed the buffer branch 2 times, most recently from 0115d98 to d0c2923 Compare January 4, 2023 14:53
@RReverser
Copy link
Contributor Author

RReverser commented Jan 4, 2023

Testing manually for now, looks like I missed some stuff. Fixing. Done.

@toyobayashi
Copy link
Owner

Thank you for your contribution.

Are they converted automatically somehow or do you rewrite them manually?

Honestly, I don't know how to use node-gyp to build emscripten project. To use the official test cases, I manually copied them to this repository, then used my toy project @tybys/cgen as the build tool, and I could use JavaScript to write the configuration (cgen.config.js) for the build. Also, since wasm loading is asynchronous, we can not directly use the require function, so I modified the code for initialization.

I will add official buffer test here later.

@RReverser
Copy link
Contributor Author

Honestly, I don't know how to use node-gyp to build emscripten project.

Heh, I actually have separate upcoming PR for that to emnapi :)

I will add official buffer test here later.

Thanks!

@RReverser
Copy link
Contributor Author

Ok fixed the problems found via tests (btw, can you enable CI for me please?).

One thing I'm not happy about and couldn't figure out is how upstream N-API implementation gets away with not truncating the NULL terminator from the char array when creating a same-sized Buffer: f043338 (#13)

That is, without me doing this -1 thing, all the buffers would have final null terminator included in the size and the tests would fail because .toString() would return Lorem ipsum...\0 instead of just Lorem ipsum.... And, logically, that seems expected, but given that this test works somehow in upstream Node, I guess they're doing something special?

@toyobayashi
Copy link
Owner

toyobayashi commented Jan 5, 2023

Oh, It's napi_create_string_utf8 bug here.

image

Without -1, binding.theText missing the \0 in emnapi test, but include \0 in native.

Seems we can not use UTF8ToString if length !== -1

@RReverser
Copy link
Contributor Author

Ah... yes, an old problem :( emscripten-core/emscripten#4693

@RReverser
Copy link
Contributor Author

Ok I removed the - 1 from the test to avoid accidentally hiding that bug, now that we know what it is.

@toyobayashi
Copy link
Owner

Before merge this PR, I have two questions ask for your opinion:

  1. Currently napi_create_external_buffer calls napi_create_external_buffer, means that the JS Buffer's memory is a copy from wasm void* data. While emnapi_create_external_uint8array create a view from wasm memory without copying. Is it better to do the similar things in napi_create_external_buffer to avoid user calling emnapi_sync_memory?
  2. napi_is_buffer is not wrapped by $PREAMBLE! (so does Node.js source code), it will throw ReferenceError if there is no window.Buffer in browser instead of returning a napi_status, while Node.js napi_is_buffer never throws, is it better to make its behavior more closer to Node.js?

@RReverser
Copy link
Contributor Author

RReverser commented Jan 6, 2023

  1. Currently napi_create_external_buffer calls napi_create_external_buffer, means that the JS Buffer's memory is a copy from wasm void* data. While emnapi_create_external_uint8array create a view from wasm memory without copying. Is it better to do the similar things in napi_create_external_buffer to avoid user calling emnapi_sync_memory?

I briefly thought about this, but there are two problems:

  1. I still don't understand finalization helpers in this codebase well enough 😅 (and, unfortunately, they're not very commented / documented, so that didn't help).

  2. I realised that emnapi_sync_memory will be necessary a bit more rarely, but it will be still necessary whenever Wasm memory is resized due to some reallocation. In fact, now that you mentioned it, I'm pretty sure that emnapi_create_external_uint8array will suffer from the same problem (unless you already support synchronising it too?).

If you reallocate, it triggers memory.grow(...) on Wasm, which, in turn, invalidates the underlying ArrayBuffer, and that, in turn, invalidates any views into it, including the view returned from emnapi_create_external_uint8array. There are some future proposals that will make it easier, but for now you need to manually re-create views whenever that happens - e.g. that's exactly what Emscripten & wasm-bindgen do to keep things like HEAPU8, HEAPU16 etc. working.

So, yes, I do think that switching to Buffer being a view would be useful - at least it would certainly be faster due to avoiding a copy - but (an enhanced version of) emnapi_sync_memory will be still necessary.

2. napi_is_buffer is not wrapped by $PREAMBLE! (so does Node.js source code), it will throw ReferenceError if there is no window.Buffer in browser instead of returning a napi_status, while Node.js napi_is_buffer never throws, is it better to make its behavior more closer to Node.js?

Ah yeah, good point - I didn't think of the missing Buffer in browser. I think just adding typeof Buffer === 'function' should be enough, let me do that done.

@RReverser
Copy link
Contributor Author

RReverser commented Jan 6, 2023

2. I'm pretty sure that emnapi_create_external_uint8array will suffer from the same problem (unless you already support synchronising it too?).

Yeah confirmed with a simple failing test: RReverser@b44912b. Feel free to cherry-pick if you want to make it work with emnapi_sync_memory somehow.

@toyobayashi
Copy link
Owner

it will be still necessary whenever Wasm memory is resized due to some reallocation

Nice, didn't realize it, thanks for pointing this out. I will try to make it work.

if (!handle.isBuffer()) {
return envObject.setLastError(napi_status.napi_invalid_arg)
}
return napi_get_typedarray_info(env, buffer, 0, length, data, 0, 0)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imported symbols start with _ at runtime

Comment on lines 281 to 282
const value = emnapiCtx.addToCurrentScope(arrayBuffer).id
$makeSetValue('result', 0, 'value', '*')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value is renamed here by macro expanding, cause makeSetValue receive a wrong variable. It should be declared outside of $PREAMBLE! macro.

The return value is an ArrayBuffer, but nodejs Buffer is a Uint8Array.

Comment on lines 309 to 328
function napi_create_external_buffer(
env: napi_env,
length: size_t,
data: Pointer<void>,
finalize_cb: napi_finalize,
finalize_hint: Pointer<void>,
result: Pointer<napi_value>
) {
$PREAMBLE!(env, (envObject) => {
const status = napi_create_external_arraybuffer(env, data, length, finalize_cb, finalize_hint, result)
if (status !== napi_status.napi_ok) {
return status
}
const handleId = $makeGetValue('result', 0, '*')
const handle = emnapiCtx.handleStore.get(handleId)!
// I know I'm the only owner of handle, so just wrap value in-place.
handle.value = Buffer.from(handle.value)
return envObject.getReturnStatus()
})
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do something like emnapi_create_external_uint8array here to avoid memory copying?

@@ -63,6 +63,10 @@ export class Handle<S> {
return !this.isEmpty() && (ArrayBuffer.isView(this.value)) && !(this.value instanceof DataView)
}

public isBuffer (): boolean {
return !this.isEmpty() && (this.value instanceof Buffer)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no globalThis.Buffer in browser environment, prefer typeof Buffer !== 'undefined' && Buffer.isBuffer(this.value)

@toyobayashi toyobayashi merged commit b2946a5 into toyobayashi:main Jan 6, 2023
@RReverser
Copy link
Contributor Author

Hm, little confused at these comments - they seem to be the ones I already found & fixed but Github shows that they were left after the PR was merged?

@RReverser RReverser deleted the buffer branch January 7, 2023 00:14
@toyobayashi
Copy link
Owner

emm, sorry, probably it was my bad, I left those comments but forgot to clicked "submit review" button at first, and clicked approve before merging. I thought you had seen it.

@RReverser
Copy link
Contributor Author

emm, sorry, probably it was my bad, I left those comments but forgot to clicked "submit review" button at first, and clicked approve before merging. I thought you had seen it.

Ah, no, I just ran into same bugs on my own after you added tests, and fixed them myself 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants