Generate Rust bindings that match Bazel-built V8 archives (#57644)

## Why

Pre-generated bindings can disagree with native V8 layouts, including CppGC member storage and the inline typed-array size. Source archives need bindings generated with the same headers, defines, and target configuration.

## What changed

- Generate bindings through Bazel using the archive's C++ compilation metadata, and include them in Darwin and Linux release pairs and source builds.
- Select native Windows MSVC archives and matching bindings together with `--//:rusty_v8_from_source=true`, and generate target-specific Windows GNU bindings.
- Pin a corrected `rusty_v8` revision for Cargo and Bazel, and track native Windows LLVM tools and resources as action inputs so pin changes invalidate cached actions.
- Run release and canary Cargo smoke tests with explicit target triples, required Linux linkers, and `--include-ignored`.

## Testing

Add `codex-v8-poc` coverage for CppGC member size, alignment, tracing, and weak references; typed-array contents and backing-store promotion; and microtask queues across garbage collection and context reassignment. Enable the otherwise ignored CppGC ABI checks for Bazel source builds and staged release smoke tests.

GitOrigin-RevId: 9edfa057a0ef2e323543bb8132c070d336ded1b2
This commit is contained in:
Sean Huang
2026-10-28 04:48:23 +00:00
committed by copyberry
parent 424916ee94
commit d63fe536ee
17 changed files with 727 additions and 175 deletions
+1 -2
View File
@@ -16599,8 +16599,7 @@ dependencies = [
[[package]]
name = "v8"
version = "152.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a10fe1a92da5c32c7c7f838ce36c0ccfcfd5edf0865b58bdde820aa64cea9888"
source = "git+https://github.com/seanh-oai/rusty_v8.git?rev=350c5a859a9b3c84ab8701b3ad51e3b7699b30c0#350c5a859a9b3c84ab8701b3ad51e3b7699b30c0"
dependencies = [
"bindgen",
"bitflags 2.13.1",
+1
View File
@@ -674,6 +674,7 @@ opt-level = 0
crossterm = { git = "https://github.com/openai-oss-forks/crossterm", rev = "ed1cdab335221515706178d68495bba2aed1924f" }
tokio-tungstenite = { git = "https://github.com/openai-oss-forks/tokio-tungstenite", rev = "0e5b2d73aa18dd9f0a50ee9ff199d5aef7594186" }
tungstenite = { git = "https://github.com/openai-oss-forks/tungstenite-rs", rev = "4fffad30fe373adbdcffab9545e9e9bf4f2fc19f" }
v8 = { git = "https://github.com/seanh-oai/rusty_v8.git", rev = "350c5a859a9b3c84ab8701b3ad51e3b7699b30c0" }
# Uncomment to debug local changes.
# rmcp = { path = "../../rust-sdk/crates/rmcp" }
+1
View File
@@ -332,6 +332,7 @@ allow-git = [
"https://github.com/openai-oss-forks/crossterm",
"https://github.com/openai-oss-forks/tokio-tungstenite",
"https://github.com/openai-oss-forks/tungstenite-rs",
"https://github.com/seanh-oai/rusty_v8.git",
]
[sources.allow-org]
+5
View File
@@ -5,6 +5,11 @@ codex_rust_crate(
crate_features = ["sandbox"],
crate_name = "codex_v8_poc",
deps_extra = ["@crates//:v8"],
# The ignored CppGC ABI checks require the native archive/binding pair.
unit_test_args = select({
"//third_party/v8:use_rusty_v8_prebuilts": [],
"//conditions:default": ["--include-ignored"],
}),
)
alias(
+321 -3
View File
@@ -47,11 +47,17 @@ mod tests {
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
evaluate_script(scope, expression).to_rust_string_lossy(scope)
}
fn evaluate_script<'s>(
scope: &mut v8::PinScope<'s, '_>,
expression: &str,
) -> v8::Local<'s, v8::Value> {
let source = v8::String::new(scope, expression).expect("expression should be valid UTF-8");
let script = v8::Script::compile(scope, source, None).expect("expression should compile");
let result = script.run(scope).expect("expression should evaluate");
result.to_rust_string_lossy(scope)
script.run(scope).expect("expression should evaluate")
}
#[test]
@@ -69,6 +75,62 @@ mod tests {
assert_eq!(super::linked_v8_has_sandbox(), cfg!(feature = "sandbox"));
}
#[test]
#[ignore = "Requires a native V8 pair and corrected rusty_v8; published 152.2.0 has an incompatible CppGC ABI"]
fn cppgc_member_storage_satisfies_alignment() {
use v8::cppgc;
struct Item;
// SAFETY: Item contains no GC references to trace.
unsafe impl cppgc::GarbageCollected for Item {
fn trace(&self, _: &mut cppgc::Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"Item"
}
}
// In pinned V8 the C++ slot is either a uint32_t or native pointer.
// No Member construction occurs until we know Rust can satisfy both.
for (name, size, alignment) in [
(
"Member",
size_of::<cppgc::Member<Item>>(),
align_of::<cppgc::Member<Item>>(),
),
(
"WeakMember",
size_of::<cppgc::WeakMember<Item>>(),
align_of::<cppgc::WeakMember<Item>>(),
),
] {
assert!(matches!(size, 4 | 8), "unexpected {name} size: {size}");
assert_eq!(
alignment, size,
"{name} storage does not meet C++ alignment"
);
}
#[repr(C)]
struct BytePrefix {
prefix: u8,
strong: cppgc::Member<Item>,
weak: cppgc::WeakMember<Item>,
}
#[repr(C)]
struct WordPrefix {
prefix: u32,
strong: cppgc::Member<Item>,
weak: cppgc::WeakMember<Item>,
}
let strong_align = align_of::<cppgc::Member<Item>>();
let weak_align = align_of::<cppgc::WeakMember<Item>>();
assert_eq!(std::mem::offset_of!(BytePrefix, strong) % strong_align, 0);
assert_eq!(std::mem::offset_of!(BytePrefix, weak) % weak_align, 0);
assert_eq!(std::mem::offset_of!(WordPrefix, strong) % strong_align, 0);
assert_eq!(std::mem::offset_of!(WordPrefix, weak) % weak_align, 0);
}
#[test]
fn evaluates_integer_addition() {
assert_eq!(evaluate_expression("1 + 2"), "3");
@@ -79,6 +141,262 @@ mod tests {
assert_eq!(evaluate_expression("'hello ' + 'world'"), "hello world");
}
#[test]
fn reads_typed_array_contents_with_the_compiled_binding_size() {
initialize_v8();
let isolate = &mut v8::Isolate::new(Default::default());
v8::scope!(let scope, isolate);
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
for length in [0, 1, 63, 64, 65] {
let expression = format!("new Uint8Array({length}).fill(90)");
let value = evaluate_script(scope, &expression);
let view = v8::Local::<v8::ArrayBufferView>::try_from(value).unwrap();
let on_heap =
v8::TYPED_ARRAY_MAX_SIZE_IN_HEAP != 0 && length <= v8::TYPED_ARRAY_MAX_SIZE_IN_HEAP;
// Check native placement before asking C++ to copy into storage:
// stale size-zero bindings must fail here rather than abort in FFI.
assert_eq!(view.has_buffer(), !on_heap, "byte length {length}");
let mut storage = vec![0; v8::TYPED_ARRAY_MAX_SIZE_IN_HEAP];
let storage_ptr = storage.as_ptr();
let contents = view.get_contents(&mut storage);
assert_eq!(contents, vec![90; length], "byte length {length}");
if on_heap {
assert_eq!(contents.as_ptr(), storage_ptr);
}
// Reading inline contents copies bytes without promoting the view.
assert_eq!(view.has_buffer(), !on_heap, "byte length {length}");
}
}
#[test]
fn typed_array_backing_store_promotion_survives_collection() {
enum Promotion {
Data,
BackingStore,
}
initialize_v8();
let isolate = &mut v8::Isolate::new(Default::default());
v8::scope!(let scope, isolate);
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
for promotion in [Promotion::Data, Promotion::BackingStore] {
let value = evaluate_script(scope, "new Uint8Array(64).fill(90)");
let view = v8::Local::<v8::ArrayBufferView>::try_from(value).unwrap();
let data = match promotion {
Promotion::Data => view.data(),
Promotion::BackingStore => {
view.get_backing_store().unwrap().data().unwrap().as_ptr()
}
};
let backing_store = view.get_backing_store().unwrap();
assert!(view.has_buffer());
assert_eq!(backing_store.data().unwrap().as_ptr(), data);
scope.low_memory_notification();
assert_eq!(view.data(), data);
assert_eq!(backing_store.data().unwrap().as_ptr(), data);
assert_eq!(
backing_store
.iter()
.map(std::cell::Cell::get)
.collect::<Vec<_>>(),
vec![90; 64]
);
}
}
#[test]
fn microtask_queue_runs_promises_across_collection_and_context_reassignment() {
initialize_v8();
let isolate = &mut v8::Isolate::new(Default::default());
let queue = v8::MicrotaskQueue::new(isolate, v8::MicrotasksPolicy::Explicit);
{
v8::scope!(let scope, isolate);
let context = v8::Context::new(scope, Default::default());
let default_queue = context.get_microtask_queue().unwrap();
context.set_microtask_queue(queue.as_ref());
{
let scope = &mut v8::ContextScope::new(scope, context);
evaluate_script(
scope,
"globalThis.results = []; \
Promise.resolve(1).then(value => results.push(value)); \
(async () => { results.push(await Promise.resolve(2)); })();",
);
scope.low_memory_notification();
assert_eq!(
evaluate_script(scope, "JSON.stringify(results)").to_rust_string_lossy(scope),
"[]"
);
queue.perform_checkpoint(scope);
assert_eq!(
evaluate_script(scope, "JSON.stringify(results)").to_rust_string_lossy(scope),
"[1,2]"
);
}
// V8 requires exiting the context before replacing its queue.
// Keep the handle alive until the context releases its queue, for
// both GC-rooted and uniquely owned queue storage.
context.set_microtask_queue(default_queue);
assert!(std::ptr::eq(
context.get_microtask_queue().unwrap(),
default_queue
));
}
drop(queue);
isolate.low_memory_notification();
}
#[test]
#[ignore = "Requires a native V8 pair and corrected rusty_v8; published 152.2.0 has an incompatible CppGC ABI"]
fn compiled_cppgc_members_match_bindings_and_survive_collection() {
use std::ffi::c_void;
use v8::cppgc;
// A small prefix exercises the Rust slot alignment before C++ access.
#[repr(C)]
struct Node {
value: u32,
next: cppgc::Member<Node>,
weak: cppgc::WeakMember<Node>,
}
// SAFETY: every Member and WeakMember owned by Node is traced.
unsafe impl cppgc::GarbageCollected for Node {
fn trace(&self, visitor: &mut cppgc::Visitor) {
visitor.trace(&self.next);
visitor.trace(&self.weak);
}
fn get_name(&self) -> &'static std::ffi::CStr {
c"v8-poc cppgc Node"
}
}
unsafe extern "C" {
fn cppgc__Member__CONSTRUCT(storage: *mut c_void, object: *mut c_void);
fn cppgc__Member__Get(storage: *const c_void) -> *mut c_void;
fn cppgc__Member__DESTRUCT(storage: *mut c_void);
fn cppgc__WeakMember__CONSTRUCT(storage: *mut c_void, object: *mut c_void);
fn cppgc__WeakMember__Get(storage: *const c_void) -> *mut c_void;
fn cppgc__WeakMember__DESTRUCT(storage: *mut c_void);
}
// Fail before accessing an incorrectly aligned Rust slot through FFI.
cppgc_member_storage_satisfies_alignment();
initialize_v8();
// Member::empty() calls C++ into a Rust stack slot sized by bindgen.
// Validate that size BEFORE calling it: a stale 4-byte binding for an
// 8-byte archive must fail an assertion, never overwrite a Rust slot.
// All supported native CPPGC pointers fit a usize. A null pointer
// initializes its full declared storage to zero in both encodings.
#[repr(C, align(16))]
struct Slot([u8; 4 * size_of::<usize>()]);
for (name, size, construct, get, destruct) in [
(
"Member",
size_of::<cppgc::Member<Node>>(),
cppgc__Member__CONSTRUCT as unsafe extern "C" fn(*mut c_void, *mut c_void),
cppgc__Member__Get as unsafe extern "C" fn(*const c_void) -> *mut c_void,
cppgc__Member__DESTRUCT as unsafe extern "C" fn(*mut c_void),
),
(
"WeakMember",
size_of::<cppgc::WeakMember<Node>>(),
cppgc__WeakMember__CONSTRUCT as unsafe extern "C" fn(*mut c_void, *mut c_void),
cppgc__WeakMember__Get as unsafe extern "C" fn(*const c_void) -> *mut c_void,
cppgc__WeakMember__DESTRUCT as unsafe extern "C" fn(*mut c_void),
),
] {
let mut slot = Slot([0xa5; 4 * size_of::<usize>()]);
assert!(
size <= size_of::<usize>(),
"unexpected {name} binding size {size}"
);
let ptr = slot.0.as_mut_ptr().cast();
// SAFETY: the aligned slot has enough room for either pointer
// encoding and its guard; the object is destroyed before reuse.
let is_null = unsafe {
construct(ptr, std::ptr::null_mut());
let is_null = get(ptr).is_null();
destruct(ptr);
is_null
};
assert!(is_null, "{name} failed to return its constructed null");
assert!(
slot.0[..size].iter().all(|byte| *byte == 0),
"{name} binding is too large"
);
assert!(
slot.0[size..].iter().all(|byte| *byte == 0xa5),
"{name} binding is too small"
);
}
let mut heap = cppgc::Heap::create(v8::V8::get_current_platform(), Default::default());
heap.enable_detached_garbage_collections_for_testing();
{
// SAFETY: each temporary pointer is kept on this stack and moved
// immediately into a traced Member or a Persistent GC root.
let root = unsafe {
let child = cppgc::make_garbage_collected(
&heap,
Node {
value: 42,
next: cppgc::Member::empty(),
weak: cppgc::WeakMember::empty(),
},
);
let weak_only = cppgc::make_garbage_collected(
&heap,
Node {
value: 7,
next: cppgc::Member::empty(),
weak: cppgc::WeakMember::empty(),
},
);
let parent = cppgc::make_garbage_collected(
&heap,
Node {
value: 1,
next: cppgc::Member::new(&child),
weak: cppgc::WeakMember::new(&weak_only),
},
);
cppgc::Persistent::new(&parent)
};
// SAFETY: root and its traced Member are the only strong pointers
// in use. The weak-only object must be collectible without stack
// scanning. No UnsafePtr from allocation is dereferenced again.
unsafe {
heap.collect_garbage_for_testing(cppgc::EmbedderStackState::NoHeapPointers);
let parent = root.get().expect("persistent root should survive");
assert_eq!(parent.value, 1);
assert_eq!(
parent
.next
.get()
.expect("traced child should survive")
.value,
42
);
assert!(
parent.weak.get().is_none(),
"weak-only target should be collected"
);
}
}
heap.terminate();
}
#[test]
fn parses_crdtp_dispatchable_messages() {
let cbor = v8::crdtp::json_to_cbor(br#"{"id":7,"method":"Runtime.evaluate","params":{}}"#)