Imported from liooil/poly (
src/jsc/bindings/v8/AGENTS.md). Install upstream withnpx skills add liooil/poly --skill v8. Copyright stays with the author.
V8 C++ API Implementation Guide
This directory contains Bun's implementation of the V8 C++ API on top of JavaScriptCore. This allows native Node.js modules that use V8 APIs to work with Bun.
Architecture Overview
Bun implements V8 APIs by creating a compatibility layer that:
- Maps V8's
Local<T>handles to JSC'sJSValuesystem - Uses handle scopes to manage memory lifetimes similar to V8
- Provides V8-compatible object layouts that inline V8 functions can read
- Manages tagged pointers for efficient value representation
For detailed background, see the blog series:
- Part 1: Introduction and challenges
- Part 2: Memory layout and object representation
- Part 3: Garbage collection and primitives
Directory Structure
src/jsc/bindings/v8/
├── v8.h # Main header with V8_UNIMPLEMENTED macro
├── v8_*.h # V8 compatibility headers
├── V8*.h # V8 class headers (Number, String, Object, etc.)
├── V8*.cpp # V8 class implementations
├── shim/ # Internal implementation details
│ ├── Handle.h # Handle and ObjectLayout implementation
│ ├── HandleScopeBuffer.h # Handle scope memory management
│ ├── TaggedPointer.h # V8-style tagged pointer implementation
│ ├── Map.h # V8 Map objects for inline function compatibility
│ ├── GlobalInternals.h # V8 global state management
│ ├── InternalFieldObject.h # Objects with internal fields
│ └── Oddball.h # Primitive values (undefined, null, true, false)
├── node.h # Node.js module registration compatibility
└── real_v8.h # Includes real V8 headers when needed
Implementing New V8 APIs
1. Create Header and Implementation Files
Create V8NewClass.h:
#pragma once
#include "v8.h"
#include "V8Local.h"
#include "V8Isolate.h"
namespace v8 {
class NewClass : public Data {
public:
BUN_EXPORT static Local<NewClass> New(Isolate* isolate, /* parameters */);
BUN_EXPORT /* return_type */ SomeMethod() const;
// Add other methods as needed
};
} // namespace v8
Create V8NewClass.cpp:
#include "V8NewClass.h"
#include "V8HandleScope.h"
#include "v8_compatibility_assertions.h"
ASSERT_V8_TYPE_LAYOUT_MATCHES(v8::NewClass)
namespace v8 {
Local<NewClass> NewClass::New(Isolate* isolate, /* parameters */)
{
// Implementation - typically:
// 1. Create JSC value
// 2. Get current handle scope
// 3. Create local handle
return isolate->currentHandleScope()->createLocal<NewClass>(isolate->vm(), /* JSC value */);
}
/* return_type */ NewClass::SomeMethod() const
{
// Implementation - typically:
// 1. Convert this Local to JSValue via localToJSValue()
// 2. Perform JSC operations
// 3. Return converted result
auto jsValue = localToJSValue();
// ... JSC operations ...
return /* result */;
}
} // namespace v8
2. Add Symbol Exports
For each new C++ method, you must add the mangled symbol names to multiple files:
a. Add to src/runtime/napi/napi_body.rs
Find the v8_api module and add entries to both the #[cfg(not(windows))] (Itanium) and #[cfg(windows)] (MSVC) extern "C" blocks:
#[cfg(not(windows))]
mod v8_api {
use core::ffi::c_void;
unsafe extern "C" {
// ... existing functions ...
pub(super) fn _ZN2v88NewClass3NewEPNS_7IsolateE/* parameters */() -> *mut c_void;
pub(super) fn _ZNK2v88NewClass10SomeMethodEv() -> *mut c_void;
}
}
#[cfg(windows)]
mod v8_api {
use core::ffi::c_void;
unsafe extern "C" {
// ... existing functions ...
#[link_name = "?New@NewClass@v8@@SA?AV?$Local@VNewClass@v8@@@2@PEAVIsolate@2@/* parameters */@Z"]
pub(super) fn NewClass_New() -> *mut c_void;
#[link_name = "?SomeMethod@NewClass@v8@@QEBA/* return_type */XZ"]
pub(super) fn NewClass_SomeMethod() -> *mut c_void;
}
}
To get the correct mangled names:
For GCC/Clang (Unix):
# Build your changes first
bun bd --help # This compiles your code
# Extract symbols
nm build/CMakeFiles/bun-debug.dir/src/jsc/bindings/v8/V8NewClass.cpp.o | grep "T _ZN2v8"
For MSVC (Windows):
# Use the provided PowerShell script in the comments:
dumpbin .\build\CMakeFiles\bun-debug.dir\src\jsc\bindings\v8\V8NewClass.cpp.obj /symbols | where-object { $_.Contains(' v8::') } | foreach-object { (($_ -split "\|")[1] -split " ")[1] } | ForEach-Object { "#[link_name = `"${_}`"] pub(super) fn ___() -> *mut c_void;" }
b. Add to Symbol Files
Add to src/symbols.txt (without leading underscore):
_ZN2v88NewClass3NewEPNS_7IsolateE...
_ZNK2v88NewClass10SomeMethodEv
Add to src/symbols.dyn (with leading underscore and semicolons):
{
__ZN2v88NewClass3NewEPNS_7IsolateE...;
__ZNK2v88NewClass10SomeMethodEv;
}
Note: src/symbols.def is Windows-only and typically doesn't contain V8 symbols.
3. Add Tests
Create tests in test/v8/v8-module/main.cpp:
void test_new_class_feature(const FunctionCallbackInfo<Value> &info) {
Isolate* isolate = info.GetIsolate();
// Test your new V8 API
Local<NewClass> obj = NewClass::New(isolate, /* parameters */);
auto result = obj->SomeMethod();
// Print results for comparison with Node.js
std::cout << "Result: " << result << std::endl;
info.GetReturnValue().Set(Undefined(isolate));
}
Add the test to the registration section:
void Init(Local<Object> exports, Local<Value> module, Local<Context> context) {
// ... existing functions ...
NODE_SET_METHOD(exports, "test_new_class_feature", test_new_class_feature);
}
Add test case to test/v8/v8.test.ts:
describe("NewClass", () => {
it("can use new feature", async () => {
await checkSameOutput("test_new_class_feature", []);
});
});
4. Handle Special Cases
Objects with Internal Fields
If implementing objects that need internal fields, extend InternalFieldObject:
// In your .h file
class MyObject : public InternalFieldObject {
// ... implementation
};
Primitive Values
For primitive values, ensure they work with the Oddball system in shim/Oddball.h.
Template Classes
For ObjectTemplate or FunctionTemplate implementations, see existing patterns in V8ObjectTemplate.cpp and V8FunctionTemplate.cpp.
Memory Management Guidelines
Handle Scopes
- All V8 values must be created within an active handle scope
- Use
isolate->currentHandleScope()->createLocal<T>()to create handles - Handle scopes automatically clean up when destroyed
JSC Integration
- Use
localToJSValue()to convert V8 handles to JSC values - Use
JSC::WriteBarrierfor heap-allocated references - Implement
visitChildren()for custom heap objects
Tagged Pointers
- Small integers (±2^31) are stored directly as Smis
- Objects use pointer tagging with map pointers
- Doubles are stored in object layouts with special maps
Testing Strategy
Comprehensive Testing
The V8 test suite compares output between Node.js and Bun for the same C++ code:
- Install Phase: Sets up identical module builds for Node.js and Bun
- Build Phase: Compiles native modules using node-gyp
- Test Phase: Runs identical C++ functions and compares output
Test Categories
- Primitives: undefined, null, booleans, numbers, strings
- Objects: creation, property access, internal fields
- Arrays: creation, length, iteration, element access
- Functions: callbacks, templates, argument handling
- Memory: handle scopes, garbage collection, external data
- Advanced: templates, inheritance, error handling
Adding New Tests
- Add C++ test function to
test/v8/v8-module/main.cpp - Register function in the module exports
- Add test case to
test/v8/v8.test.tsusingcheckSameOutput() - Run with:
bun bd test test/v8/v8.test.ts -t "your test name"
Debugging Tips
Build and Test
# Build debug version (takes ~5 minutes)
bun bd --help
# Run V8 tests
bun bd test test/v8/v8.test.ts
# Run specific test
bun bd test test/v8/v8.test.ts -t "can create small integer"
Common Issues
Symbol Not Found: Ensure mangled names are correctly added to napi_body.rs and symbol files.
Segmentation Fault: Usually indicates inline V8 functions are reading incorrect memory layouts. Check Map setup and ObjectLayout structure.
GC Issues: Objects being freed prematurely. Ensure proper WriteBarrier usage and visitChildren() implementation.
Type Mismatches: Use v8_compatibility_assertions.h macros to verify type layouts match V8 expectations.
Debug Logging
Use V8_UNIMPLEMENTED() macro for functions not yet implemented:
void MyClass::NotYetImplemented() {
V8_UNIMPLEMENTED();
}
Advanced Topics
Inline Function Compatibility
Many V8 functions are inline and compiled into native modules. The memory layout must exactly match what these functions expect:
- Objects start with tagged pointer to
Map - Maps have instance type at offset 12
- Handle scopes store tagged pointers
- Primitive values at fixed global offsets
Cross-Platform Considerations
- Symbol mangling differs between GCC/Clang and MSVC
- Handle calling conventions (JSC uses System V on Unix)
- Ensure
BUN_EXPORTvisibility on all public functions - Test on all target platforms via CI
Contributing
When contributing V8 API implementations:
- Follow existing patterns in similar classes
- Add comprehensive tests that compare with Node.js
- Update all symbol files with correct mangled names
- Document any special behavior or limitations
For questions about V8 API implementation, refer to the blog series linked above or examine existing implementations in this directory.