Skip to content
New issue

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

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

Already on GitHub? # to your account

ArrayBuffer and Binary json encoding #111

Merged
merged 13 commits into from
Jul 19, 2023

Conversation

yourWaifu
Copy link
Contributor

@yourWaifu yourWaifu commented Apr 5, 2023

I needed this for a project, and so I implemented them for myself. However, I noticed that others asked about it here, #68, so I decided to share them.

Example:

socket.on("data", (chunk) => {
  // imagine IPC between a program that sends their chunks with a 4 byte length little endian prefix
  const lengthLength = 4;
  let length = 0;
  for (var i = 0; i < lengthLength; i += 1) {
	  length = (length * 256) + chunk[i];
  }
  
  // create QuickJS ArrayBuffer
  let bufferStart = lengthLength;
  let bufferEnd = length + lengthLength;
  this._contextJS.newArrayBuffer(new Uint8Array(chunk.slice(bufferStart, bufferEnd)))
  .consume((handle) => {
	  // decode data inside the buffer
	  this._contextJS?.decodeBinaryJSON(handle)
		  .consume((handle) => console.log(this._contextJS?.dump(handle)));
  });

@yourWaifu yourWaifu changed the title Binary json encoding ArrayBuffer and Binary json encoding Apr 5, 2023
Copy link
Owner

@justjake justjake left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! Your changes look good. I have a suggestion for improving the efficiency of copying a ArrayBuffer into QuickJS, and it would also be nice to see some tests.

*
* **WARNING**: QuickJS's binary JSON doesn't have a standard so expect it to change between version
*/
encodeBinaryJSON(handle: QuickJSHandle): QuickJSHandle {
Copy link
Owner

Choose a reason for hiding this comment

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

It would be nice to include a comment in the code showing how to use this that shows up in the documentation. How would I persist or transfer this value over the network? What's the use-case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mostly have been using decodeBinaryJSON to handle IPC between an app that uses quickJS and one that uses node.js. However, I realized that encodeBinaryJSON isn't all that useful without some sort of getArrayBuffer function. I've been trying to write one, but it seems to not want to work.

c/interface.c Outdated
}

// --------------------
// Debugging QuickJS values
Copy link
Owner

Choose a reason for hiding this comment

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

This comment doesn't make sense to me

c/interface.c Outdated
@@ -301,6 +301,10 @@ JSValue *QTS_NewArray(JSContext *ctx) {
return jsvalue_to_heap(JS_NewArray(ctx));
}

JSValue *QTS_NewArrayBuffer(JSContext *ctx, JSVoid *buffer, size_t length) {
return jsvalue_to_heap(JS_NewArrayBufferCopy(ctx, (uint8_t*)buffer, length));
Copy link
Owner

Choose a reason for hiding this comment

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

This method always does an extra copy:

  1. Copy from source ArrayBuffer to Emscripten memory at the _malloc pointer
  2. Copy from the _malloc pointer to the quickjs ArrayBuffer

We can eliminate copy 2 by using JS_NewArrayBuffer to take ownership of the already malloc'd WebAssembly memory. You'd need to define a JSFreeArrayBufferDataFunc, but it'd be super simple, something like void qts_free_buffer(JSRuntime *unused_rt, void *unused_opaque, void *ptr) { free(ptr) }. Then skip wrapping the malloc pointer in a lifetime and you're good to go.

@yourWaifu
Copy link
Contributor Author

yourWaifu commented Apr 13, 2023

I added a getArrayBuffer function, but the values outputed by it seem to be incorrect for unknown reasons, so we should put this into draft until that can be fixed

@justjake
Copy link
Owner

If the array buffer creation bit works well, can we merge that without the JSONB parts?

@yourWaifu
Copy link
Contributor Author

yourWaifu commented Apr 16, 2023

After some experiments, I noticed that NewArrayBufferCopy and NewArrayBuffer creates different outputs when using GetArrayBuffer. I'm guessing you are supposed to read the buffer differently but not sure how the code knows the difference and how it reads NewArrayBuffer. Using NewArrayBufferCopy fixes all the issues with GetArrayBuffer.

ts/context.ts Outdated
.newHeapBufferPointer(array)
.consume((bufferHandle) => this.ffi.QTS_NewArrayBuffer(this.ctx.value, bufferHandle.value.pointer, array.length))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

removing this line seemed to fix the issue. Not sure if it'll cause more issues, but it seems to be fine

@yourWaifu yourWaifu requested a review from justjake April 24, 2023 15:27
Copy link
Owner

@justjake justjake left a comment

Choose a reason for hiding this comment

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

I would love to see some tests for this stuff. Other than that, it looks ready to merge.

Comment on lines +342 to +352
// I don't know how to return two values in C, maybe allocate memory in stack?
size_t QTS_GetArrayBufferLength(JSContext *ctx, JSValueConst *data) {
Copy link
Owner

Choose a reason for hiding this comment

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

I think we could add stackAlloc and use it to pass in a *size_t argument like the &length arguments in QuickJS's own API, but it's not document anywhere so I think your current approach is fine.

Copy link
Contributor Author

@yourWaifu yourWaifu Apr 30, 2023

Choose a reason for hiding this comment

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

Looking online, the best solutions I could find is to use Emscripten's embind API and a separate cpp file to use the API. However, I'm not familiar with Makefile, so I don't really know how to get it to compile the separate cpp file.

I can find what the command should look like:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

But not how to do it using Make because I get this error

emcc: error: cannot specify -o with -c/-S/-E/-M and multiple source files

Removing -c gets me this error

wasm-ld: error: unknown file type: build/wrapper/interface.WASM_DEBUG_SYNC.o

@yourWaifu yourWaifu force-pushed the binary-json-encoding branch from 56146eb to b6a8a04 Compare May 5, 2023 22:14
@yourWaifu
Copy link
Contributor Author

It still says awaiting for approval

@sgrove
Copy link

sgrove commented Jul 19, 2023

I also came across a use case where I need to share an ArrayBuffer between host and client, it'd be great to see this merged in!

@justjake justjake merged commit 24c340c into justjake:main Jul 19, 2023
@stickmy
Copy link

stickmy commented Aug 28, 2023

I has the same scenario, but i went some issues.

javascript code executed in quickjs

console.log(new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]));

When i use vm.getArrayBuffer() in console.log implementation, i got a error:
Couldn't allocate memory to get ArrayBuffer at QuickJSContext.getArrayBuffer

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

Successfully merging this pull request may close these issues.

4 participants