fix(ext/napi): implement zero-copy external Latin-1 strings#33283
Open
bartlomieju wants to merge 2 commits intomainfrom
Open
fix(ext/napi): implement zero-copy external Latin-1 strings#33283bartlomieju wants to merge 2 commits intomainfrom
bartlomieju wants to merge 2 commits intomainfrom
Conversation
node_api_create_external_string_latin1 previously always copied the string data and immediately called the finalize callback. This implements true zero-copy for Latin-1 strings using V8's external string API (v8::String::new_external_onebyte_raw). When a finalize callback is provided, V8 takes ownership of the buffer and calls a destructor when the string is garbage collected. The destructor invokes the NAPI finalize callback via a global metadata map keyed by buffer address. Falls back to the copy path if: - No finalize callback provided (can't guarantee buffer lifetime) - V8 rejects the external string (e.g. buffer too small) node_api_create_external_string_utf16 still copies because rusty_v8 doesn't expose a non-static external two-byte string API. The *copied flag and null check are now handled correctly for both. Tests: - test_external_latin1: creates an external Latin-1 string, verifies content roundtrip, reports zero-copy status Refs: Node.js src/js_native_api_v8.cc ExternalOneByteStringResource Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bartlomieju
commented
Apr 15, 2026
| }; | ||
| if let Some(v8_str) = v8_str { | ||
| // Register the finalize callback | ||
| EXTERNAL_STRING_METADATA.lock().unwrap().insert( |
Member
Author
There was a problem hiding this comment.
This should probably be stored on the Env not globally
The test declared node_api_create_external_string_latin1 via a manual extern "C" block, which works on macOS/Linux (flat namespace) but fails on Windows because the symbol needs to come through the NAPI symbol resolution mechanism. Fix: add node_api_create_external_string_latin1 and node_api_create_external_string_utf16 declarations to libs/napi_sys/src/functions.rs so they're properly resolved via dlopen on all platforms. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bartlomieju
commented
Apr 15, 2026
Comment on lines
+1422
to
+1423
| // rusty_v8 doesn't expose a non-static external two-byte string API, | ||
| // so we always copy for UTF-16 strings. |
bartlomieju
added a commit
to denoland/rusty_v8
that referenced
this pull request
Apr 15, 2026
Add two-byte (UTF-16) equivalents of the existing external one-byte string APIs: - new_external_twobyte: creates a v8::String from a Box<[u16]>, V8 takes ownership and frees via free_rust_external_twobyte on GC - new_external_twobyte_raw: creates a v8::String from a raw *mut u16 with a custom destructor, called when the string is GC'd These mirror new_external_onebyte and new_external_onebyte_raw. The C++ binding adds ExternalTwoByteString which implements v8::String::ExternalStringResource, tracks external memory via AdjustAmountOfExternalAllocatedMemory, and calls the Rust destructor. Needed by denoland/deno#33283 for zero-copy UTF-16 external strings in the Node-API implementation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bartlomieju
added a commit
to denoland/rusty_v8
that referenced
this pull request
Apr 16, 2026
Add two-byte (UTF-16) equivalents of the existing external one-byte string APIs: - new_external_twobyte: creates a v8::String from a Box<[u16]>, V8 takes ownership and frees via free_rust_external_twobyte on GC - new_external_twobyte_raw: creates a v8::String from a raw *mut u16 with a custom destructor, called when the string is GC'd These mirror new_external_onebyte and new_external_onebyte_raw. The C++ binding adds ExternalTwoByteString which implements v8::String::ExternalStringResource, tracks external memory via AdjustAmountOfExternalAllocatedMemory, and calls the Rust destructor. Needed by denoland/deno#33283 for zero-copy UTF-16 external strings in the Node-API implementation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
node_api_create_external_string_latin1previously always copied the string data and immediately called the finalize callback. This implements true zero-copy using V8's external one-byte string API.node_api_create_external_string_utf16still copies because rusty_v8 doesn't expose a non-static external two-byte string API. The*copiedflag is set correctly so well-behaved callers handle both cases.How it works
When a finalize callback is provided:
v8::String::new_external_onebyte_raw()pointing directly to the caller's buffer (zero-copy)*copied = falseFalls back to copy path when:
Refs: Node.js
ExternalOneByteStringResourceTest plan
test_external_latin1-- creates external Latin-1 string, verifies content roundtrip./x test-napi-- 115 passed, 1 pre-existing failure./x lint --jspasses🤖 Generated with Claude Code