Skip to content

Commit 442f7ae

Browse files
committed
feat: arrayBufferFromBuffer
1 parent caff5a8 commit 442f7ae

15 files changed

Lines changed: 201 additions & 1 deletion

File tree

runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ features = [
4646
"Win32_UI_Shell",
4747
"Win32_UI_WindowsAndMessaging",
4848
"System",
49+
"Storage_Streams",
4950
"Data_Json",
5051
"UI_Composition",
5152
"Foundation_Numerics",

runtime/src/global_fns.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
66
use std::process::Command;
77
use std::time::{Duration, Instant};
88
use windows::core::{IUnknown, Interface};
9+
use windows::Storage::Streams::IBuffer;
10+
use windows::Win32::System::WinRT::IBufferByteAccess;
911
use windows::Win32::UI::WindowsAndMessaging::{
1012
DispatchMessageW, PeekMessageW, TranslateMessage, MSG, PM_REMOVE,
1113
};
@@ -29,6 +31,19 @@ thread_local!(static DEVTOOLS_SERVER: RefCell<Option<DevtoolsServer>> = RefCell:
2931

3032
thread_local!(static INSPECTOR_DOMAIN_DISPATCHERS: RefCell<HashMap<String, v8::Global<v8::Value>>> = RefCell::new(HashMap::new()));
3133

34+
// Set once `Runtime::drop` has torn down the WinRT apartment (RoUninitialize).
35+
// Backing-store deleters that hold COM refs consult this so they skip the
36+
// Release during isolate disposal (which runs after RoUninitialize): the OS
37+
// reclaims those refs at process exit, and calling Release on a dead apartment
38+
// would access-violate. During normal GC the flag is false and refs are freed.
39+
thread_local!(static COM_TEARDOWN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) });
40+
41+
/// Mark that this thread's COM apartment is being torn down. Called from
42+
/// `Runtime::new` (reset) and `Runtime::drop` (set).
43+
pub(crate) fn set_com_teardown(value: bool) {
44+
COM_TEARDOWN.with(|c| c.set(value));
45+
}
46+
3247
/// Drop cached inspector dispatcher handles (isolate-tied `v8::Global`s).
3348
/// Called from `Runtime::drop` while the isolate is still alive.
3449
pub(crate) fn clear_thread_dispatchers() {
@@ -458,6 +473,109 @@ pub(crate) fn handle_buffer_to_pointer(
458473
}
459474
}
460475

476+
/// Backing-store deleter that releases the COM reference kept alive for the
477+
/// lifetime of the aliasing ArrayBuffer. `deleter_data` is a `Box<IBuffer>`.
478+
unsafe extern "C" fn release_ibuffer_keepalive(
479+
_data: *mut c_void,
480+
_byte_length: usize,
481+
deleter_data: *mut c_void,
482+
) {
483+
if deleter_data.is_null() {
484+
return;
485+
}
486+
let keep_alive = unsafe { Box::from_raw(deleter_data as *mut IBuffer) };
487+
if COM_TEARDOWN.with(|c| c.get()) {
488+
// Apartment is gone; releasing now would access-violate. Leak the ref —
489+
// the process is exiting and the OS reclaims it.
490+
std::mem::forget(keep_alive);
491+
} else {
492+
drop(keep_alive);
493+
}
494+
}
495+
496+
/// `__nsArrayBufferFromBuffer(buffer)` — wrap a `Windows.Storage.Streams.IBuffer`
497+
/// as a zero-copy `ArrayBuffer` aliasing the buffer's native storage. The ArrayBuffer
498+
/// covers `IBuffer.Length` (valid bytes). The IBuffer is AddRef'd and released only
499+
/// once V8 collects the ArrayBuffer, so reads/writes through the view are safe and
500+
/// propagate to the underlying WinRT buffer.
501+
pub(crate) fn handle_array_buffer_from_buffer(
502+
scope: &mut v8::PinScope<'_, '_>,
503+
args: v8::FunctionCallbackArguments,
504+
mut retval: v8::ReturnValue,
505+
) {
506+
if args.length() < 1 {
507+
throw_js_error(
508+
scope,
509+
"__nsArrayBufferFromBuffer expects a Windows.Storage.Streams.IBuffer",
510+
);
511+
return;
512+
}
513+
514+
let Ok(obj) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
515+
throw_js_error(
516+
scope,
517+
"__nsArrayBufferFromBuffer expects a WinRT IBuffer object",
518+
);
519+
return;
520+
};
521+
522+
let Some(instance) = crate::ns_proxy::this_instance(scope, obj) else {
523+
throw_js_error(
524+
scope,
525+
"__nsArrayBufferFromBuffer: value is not a WinRT object (no COM instance)",
526+
);
527+
return;
528+
};
529+
530+
// QI for the valid length and the raw data pointer. Both views address the
531+
// same underlying object, so a single AddRef'd ref keeps the storage alive.
532+
let Ok(buffer) = instance.cast::<IBuffer>() else {
533+
throw_js_error(
534+
scope,
535+
"__nsArrayBufferFromBuffer: value does not implement Windows.Storage.Streams.IBuffer",
536+
);
537+
return;
538+
};
539+
let Ok(byte_access) = instance.cast::<IBufferByteAccess>() else {
540+
throw_js_error(
541+
scope,
542+
"__nsArrayBufferFromBuffer: IBuffer does not expose IBufferByteAccess",
543+
);
544+
return;
545+
};
546+
547+
let len = buffer.Length().unwrap_or(0) as usize;
548+
let data = match unsafe { byte_access.Buffer() } {
549+
Ok(p) => p,
550+
Err(_) => {
551+
throw_js_error(scope, "__nsArrayBufferFromBuffer: failed to obtain IBuffer data pointer");
552+
return;
553+
}
554+
};
555+
556+
if data.is_null() || len == 0 {
557+
// Nothing to alias — hand back an empty, owned ArrayBuffer.
558+
let ab = v8::ArrayBuffer::new(scope, 0);
559+
retval.set(ab.into());
560+
return;
561+
}
562+
563+
// Hand an AddRef'd IBuffer to the deleter so the native storage outlives the
564+
// ArrayBuffer; `byte_access` is dropped here but the object stays alive.
565+
let keep_alive = Box::into_raw(Box::new(buffer)) as *mut c_void;
566+
let backing = unsafe {
567+
v8::ArrayBuffer::new_backing_store_from_ptr(
568+
data as *mut c_void,
569+
len,
570+
release_ibuffer_keepalive,
571+
keep_alive,
572+
)
573+
};
574+
let shared = backing.make_shared();
575+
let ab = v8::ArrayBuffer::with_backing_store(scope, &shared);
576+
retval.set(ab.into());
577+
}
578+
461579
pub(crate) fn handle_proxy_write_text_file(
462580
scope: &mut v8::PinScope<'_, '_>,
463581
args: v8::FunctionCallbackArguments,
@@ -1488,6 +1606,20 @@ const HELPER_SOURCE: &str = r#"
14881606
fromWinRTDateTimeTicks: fromWinRTDateTimeTicks,
14891607
pointerKey: pointerKey,
14901608
pointerFromBuffer: pointerFromBuffer,
1609+
// arrayBufferFromBuffer(buffer) — zero-copy view over a
1610+
// Windows.Storage.Streams.IBuffer. The returned ArrayBuffer aliases
1611+
// the buffer's native storage (covers IBuffer.Length valid bytes) and
1612+
// keeps the IBuffer alive until it is collected. Use `.byteLength` on
1613+
// the result, or wrap in a Uint8Array/DataView to read or write through.
1614+
arrayBufferFromBuffer: function (buffer) {
1615+
if (buffer == null) {
1616+
return null;
1617+
}
1618+
if (typeof globalThis.__nsArrayBufferFromBuffer !== 'function') {
1619+
throw new Error('NSWinRT.interop.arrayBufferFromBuffer is unavailable in this runtime');
1620+
}
1621+
return globalThis.__nsArrayBufferFromBuffer(buffer);
1622+
},
14911623
trackBufferSource: trackBufferSource,
14921624
resolveTrackedBuffer: resolveTrackedBuffer,
14931625
byteLengthOf: function (value) {
@@ -4615,6 +4747,7 @@ pub(crate) fn init_async_helpers(
46154747
register!("__nsEnqueueMicrotask", handle_enqueue_microtask);
46164748
register!("__nsPointerKey", handle_pointer_key);
46174749
register!("__nsBufferToPointer", handle_buffer_to_pointer);
4750+
register!("__nsArrayBufferFromBuffer", handle_array_buffer_from_buffer);
46184751
register!("__nsProxyWriteTextFile", handle_proxy_write_text_file);
46194752
register!("__nsProxyCompileProject", handle_proxy_compile_project);
46204753
register!("__nsProxyRegisterManifest", handle_proxy_register_manifest);

runtime/src/interop_test.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,63 @@ fn typed_array_to_windows_buffer_exposes_length() {
452452
);
453453
}
454454

455+
#[test]
456+
fn windows_buffer_to_array_buffer_zero_copy_read() {
457+
run_js_assert(
458+
"windows_buffer_to_array_buffer_zero_copy_read",
459+
r#"
460+
const cb = Windows.Security.Cryptography.CryptographicBuffer;
461+
const input = new Uint8Array([1, 2, 3, 254]);
462+
const winBuf = cb.CreateFromByteArray(input);
463+
464+
const ab = interop.arrayBufferFromBuffer(winBuf);
465+
if (!(ab instanceof ArrayBuffer)) {
466+
throw new Error(`Expected ArrayBuffer, got ${Object.prototype.toString.call(ab)}`);
467+
}
468+
if (ab.byteLength !== 4) {
469+
throw new Error(`Expected byteLength=4, got ${ab.byteLength}`);
470+
}
471+
472+
const view = new Uint8Array(ab);
473+
const expected = [1, 2, 3, 254];
474+
for (let i = 0; i < expected.length; i++) {
475+
if (view[i] !== expected[i]) {
476+
throw new Error(`Mismatch at ${i}: got ${view[i]}, expected ${expected[i]}`);
477+
}
478+
}
479+
"#,
480+
);
481+
}
482+
483+
#[test]
484+
fn windows_buffer_to_array_buffer_writes_through() {
485+
run_js_assert(
486+
"windows_buffer_to_array_buffer_writes_through",
487+
r#"
488+
const cb = Windows.Security.Cryptography.CryptographicBuffer;
489+
const input = new Uint8Array([0, 0, 0, 0]);
490+
const winBuf = cb.CreateFromByteArray(input);
491+
492+
// Mutate through the zero-copy view, then read the bytes back out of the
493+
// WinRT buffer via DataReader to prove the write aliased native storage.
494+
const view = new Uint8Array(interop.arrayBufferFromBuffer(winBuf));
495+
view[0] = 10;
496+
view[1] = 20;
497+
view[2] = 30;
498+
view[3] = 40;
499+
500+
const reader = Windows.Storage.Streams.DataReader.FromBuffer(winBuf);
501+
const got = [reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte()];
502+
const expected = [10, 20, 30, 40];
503+
for (let i = 0; i < expected.length; i++) {
504+
if (got[i] !== expected[i]) {
505+
throw new Error(`Mismatch at ${i}: got ${got[i]}, expected ${expected[i]}`);
506+
}
507+
}
508+
"#,
509+
);
510+
}
511+
455512
#[test]
456513
fn native_wrong_type_throws() {
457514
run_js_assert(

runtime/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7531,6 +7531,10 @@ impl Runtime {
75317531
v8::V8::initialize();
75327532
});
75337533

7534+
// A prior Runtime on this thread may have set the teardown flag; clear it
7535+
// so this runtime's backing-store deleters release COM refs normally.
7536+
crate::global_fns::set_com_teardown(false);
7537+
75347538
let winrt_initialized = match unsafe { RoInitialize(RO_INIT_SINGLETHREADED) } {
75357539
Ok(_) => true,
75367540
// Any failure (including RPC_E_CHANGED_MODE=0x80010106 and the ASTA apartment
@@ -8034,6 +8038,10 @@ impl Drop for Runtime {
80348038
// stops; reset so a future Runtime on this thread can schedule drains.
80358039
MICROTASK_DRAIN_QUEUED.with(|cell| cell.set(false));
80368040
if self.winrt_initialized {
8041+
// Signal backing-store deleters (e.g. zero-copy IBuffer ArrayBuffers)
8042+
// to skip COM Release once the isolate is disposed below — that runs
8043+
// after RoUninitialize, so Release would hit a dead apartment.
8044+
crate::global_fns::set_com_teardown(true);
80378045
unsafe { RoUninitialize() };
80388046
}
80398047
}
5 KB
Binary file not shown.
4.5 KB
Binary file not shown.
5 KB
Binary file not shown.
4.5 KB
Binary file not shown.
0 Bytes
Binary file not shown.
-4 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)