This commit is contained in:
pfg
2025-11-17 18:59:56 -08:00
parent 9513c1d1d9
commit ba2c6ca29d
6 changed files with 368 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
#include "root.h"
#include "SnapshotSerializers.h"
#include <JavaScriptCore/JSArray.h>
#include <JavaScriptCore/JSCJSValueInlines.h>
#include <JavaScriptCore/ObjectConstructor.h>
#include <JavaScriptCore/JSCInlines.h>
#include <JavaScriptCore/Exception.h>
#include "ErrorCode.h"
namespace Bun {
using namespace JSC;
const ClassInfo SnapshotSerializers::s_info = { "SnapshotSerializers"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(SnapshotSerializers) };
SnapshotSerializers::SnapshotSerializers(VM& vm, Structure* structure)
: Base(vm, structure)
{
}
void SnapshotSerializers::finishCreation(VM& vm)
{
Base::finishCreation(vm);
// Initialize empty arrays
m_testCallbacks.set(vm, this, JSC::constructEmptyArray(this->globalObject(), nullptr, 0));
m_serializeCallbacks.set(vm, this, JSC::constructEmptyArray(this->globalObject(), nullptr, 0));
}
template<typename Visitor>
void SnapshotSerializers::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
SnapshotSerializers* thisObject = jsCast<SnapshotSerializers*>(cell);
ASSERT_GC_OBJECT_INHERITS(thisObject, info());
Base::visitChildren(thisObject, visitor);
visitor.append(thisObject->m_testCallbacks);
visitor.append(thisObject->m_serializeCallbacks);
}
DEFINE_VISIT_CHILDREN(SnapshotSerializers);
SnapshotSerializers* SnapshotSerializers::create(VM& vm, Structure* structure)
{
SnapshotSerializers* serializers = new (NotNull, allocateCell<SnapshotSerializers>(vm)) SnapshotSerializers(vm, structure);
serializers->finishCreation(vm);
return serializers;
}
bool SnapshotSerializers::addSerializer(JSGlobalObject* globalObject, JSValue testCallback, JSValue serializeCallback)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Check for re-entrancy
if (m_isExecuting) {
throwTypeError(globalObject, scope, "Cannot add snapshot serializer from within a test or serialize callback"_s);
return false;
}
// Validate that both callbacks are callable
if (!testCallback.isCallable()) {
throwTypeError(globalObject, scope, "Snapshot serializer test callback must be a function"_s);
return false;
}
if (!serializeCallback.isCallable()) {
throwTypeError(globalObject, scope, "Snapshot serializer serialize callback must be a function"_s);
return false;
}
// Get the arrays
JSArray* testCallbacks = m_testCallbacks.get();
JSArray* serializeCallbacks = m_serializeCallbacks.get();
if (!testCallbacks || !serializeCallbacks) {
throwOutOfMemoryError(globalObject, scope);
return false;
}
// Add to the end of the arrays (most recent last, we'll iterate in reverse)
testCallbacks->push(globalObject, testCallback);
RETURN_IF_EXCEPTION(scope, false);
serializeCallbacks->push(globalObject, serializeCallback);
RETURN_IF_EXCEPTION(scope, false);
return true;
}
JSValue SnapshotSerializers::serialize(JSGlobalObject* globalObject, JSValue value)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// Check for re-entrancy
if (m_isExecuting) {
throwTypeError(globalObject, scope, "Cannot serialize from within a test or serialize callback"_s);
return jsNull();
}
// RAII guard to manage m_isExecuting flag
class ExecutionGuard {
public:
ExecutionGuard(bool& flag) : m_flag(flag) { m_flag = true; }
~ExecutionGuard() { m_flag = false; }
private:
bool& m_flag;
};
ExecutionGuard guard(m_isExecuting);
JSArray* testCallbacks = m_testCallbacks.get();
JSArray* serializeCallbacks = m_serializeCallbacks.get();
if (!testCallbacks || !serializeCallbacks) {
return jsNull();
}
unsigned length = testCallbacks->length();
// Iterate through serializers in reverse order (most recent to least recent)
for (int i = static_cast<int>(length) - 1; i >= 0; i--) {
JSValue testCallback = testCallbacks->getIndex(globalObject, static_cast<unsigned>(i));
RETURN_IF_EXCEPTION(scope, {});
if (!testCallback.isCallable()) {
continue;
}
// Call the test function with the value
auto callData = JSC::getCallData(testCallback);
MarkedArgumentBuffer args;
args.append(value);
ASSERT(!args.hasOverflowed());
JSValue testResult = call(globalObject, testCallback, callData, jsUndefined(), args);
RETURN_IF_EXCEPTION(scope, {});
// If the test returns truthy, use this serializer
if (testResult.toBoolean(globalObject)) {
RETURN_IF_EXCEPTION(scope, {});
JSValue serializeCallback = serializeCallbacks->getIndex(globalObject, static_cast<unsigned>(i));
RETURN_IF_EXCEPTION(scope, {});
if (!serializeCallback.isCallable()) {
continue;
}
// Call the serialize function with the value
auto serializeCallData = JSC::getCallData(serializeCallback);
MarkedArgumentBuffer serializeArgs;
serializeArgs.append(value);
ASSERT(!serializeArgs.hasOverflowed());
JSValue result = call(globalObject, serializeCallback, serializeCallData, jsUndefined(), serializeArgs);
RETURN_IF_EXCEPTION(scope, {});
// Return the serialized result (should be a string or null)
RELEASE_AND_RETURN(scope, result);
}
}
// No matching serializer found
return jsNull();
}
} // namespace Bun
using namespace Bun;
using namespace JSC;
// Zig-exported functions
extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__add(
Zig::GlobalObject* globalObject,
JSC::EncodedJSValue encodedSerializers,
JSC::EncodedJSValue encodedTestCallback,
JSC::EncodedJSValue encodedSerializeCallback)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue serializersValue = JSValue::decode(encodedSerializers);
SnapshotSerializers* serializers = jsDynamicCast<SnapshotSerializers*>(serializersValue);
if (!serializers) {
throwTypeError(globalObject, scope, "Invalid SnapshotSerializers object"_s);
return JSValue::encode(jsUndefined());
}
JSValue testCallback = JSValue::decode(encodedTestCallback);
JSValue serializeCallback = JSValue::decode(encodedSerializeCallback);
bool success = serializers->addSerializer(globalObject, testCallback, serializeCallback);
RETURN_IF_EXCEPTION(scope, {});
if (success) {
RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined()));
}
return JSValue::encode(jsUndefined());
}
extern "C" [[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__serialize(
Zig::GlobalObject* globalObject,
JSC::EncodedJSValue encodedSerializers,
JSC::EncodedJSValue encodedValue)
{
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue serializersValue = JSValue::decode(encodedSerializers);
SnapshotSerializers* serializers = jsDynamicCast<SnapshotSerializers*>(serializersValue);
if (!serializers) {
throwTypeError(globalObject, scope, "Invalid SnapshotSerializers object"_s);
return JSValue::encode(jsNull());
}
JSValue value = JSValue::decode(encodedValue);
JSValue result = serializers->serialize(globalObject, value);
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, JSValue::encode(result));
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include "root.h"
#include <JavaScriptCore/JSDestructibleObject.h>
#include <JavaScriptCore/JSArray.h>
#include <JavaScriptCore/WriteBarrier.h>
#include "ZigGlobalObject.h"
namespace Bun {
class SnapshotSerializers final : public JSC::JSDestructibleObject {
public:
using Base = JSC::JSDestructibleObject;
static constexpr unsigned StructureFlags = Base::StructureFlags;
static SnapshotSerializers* create(JSC::VM& vm, JSC::Structure* structure);
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
}
template<typename MyClassT, JSC::SubspaceAccess mode>
static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm)
{
if constexpr (mode == JSC::SubspaceAccess::Concurrently)
return nullptr;
return WebCore::subspaceForImpl<MyClassT, WebCore::UseCustomHeapCellType::No>(
vm,
[](auto& spaces) { return spaces.m_clientSubspaceForSnapshotSerializers.get(); },
[](auto& spaces, auto&& space) { spaces.m_clientSubspaceForSnapshotSerializers = std::forward<decltype(space)>(space); },
[](auto& spaces) { return spaces.m_subspaceForSnapshotSerializers.get(); },
[](auto& spaces, auto&& space) { spaces.m_subspaceForSnapshotSerializers = std::forward<decltype(space)>(space); });
}
DECLARE_INFO;
DECLARE_VISIT_CHILDREN;
// Add a new snapshot serializer
// Returns true on success, false if in re-entrant call (and throws)
bool addSerializer(JSC::JSGlobalObject* globalObject, JSC::JSValue testCallback, JSC::JSValue serializeCallback);
// Test a value and serialize if a matching serializer is found
// Returns the serialized string or null
JSC::JSValue serialize(JSC::JSGlobalObject* globalObject, JSC::JSValue value);
private:
SnapshotSerializers(JSC::VM& vm, JSC::Structure* structure);
void finishCreation(JSC::VM& vm);
// Arrays stored in reverse order (most recent first for iteration)
JSC::WriteBarrier<JSC::JSArray> m_testCallbacks;
JSC::WriteBarrier<JSC::JSArray> m_serializeCallbacks;
// Re-entrancy guard
bool m_isExecuting { false };
};
} // namespace Bun
// Exposed to Zig
extern "C" {
[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__add(
Zig::GlobalObject* globalObject,
JSC::EncodedJSValue encodedSerializers,
JSC::EncodedJSValue encodedTestCallback,
JSC::EncodedJSValue encodedSerializeCallback);
[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue SnapshotSerializers__serialize(
Zig::GlobalObject* globalObject,
JSC::EncodedJSValue encodedSerializers,
JSC::EncodedJSValue encodedValue);
} // extern "C"

View File

@@ -0,0 +1,62 @@
// Example Zig bindings for SnapshotSerializers
// This shows how to use the exported C++ functions from Zig
const std = @import("std");
const bun = @import("root").bun;
const JSC = bun.JSC;
// Import the exported C++ functions
extern "c" fn SnapshotSerializers__add(
globalObject: *JSC.JSGlobalObject,
serializers: JSC.JSValue,
testCallback: JSC.JSValue,
serializeCallback: JSC.JSValue,
) JSC.JSValue;
extern "c" fn SnapshotSerializers__serialize(
globalObject: *JSC.JSGlobalObject,
serializers: JSC.JSValue,
value: JSC.JSValue,
) JSC.JSValue;
/// Add a snapshot serializer
pub fn addSerializer(
global: *JSC.JSGlobalObject,
serializers: JSC.JSValue,
test_callback: JSC.JSValue,
serialize_callback: JSC.JSValue,
) JSC.JSValue {
return SnapshotSerializers__add(
global,
serializers,
test_callback,
serialize_callback,
);
}
/// Serialize a value using the registered serializers
pub fn serialize(
global: *JSC.JSGlobalObject,
serializers: JSC.JSValue,
value: JSC.JSValue,
) JSC.JSValue {
return SnapshotSerializers__serialize(
global,
serializers,
value,
);
}
// Example usage:
//
// const serializers = SnapshotSerializers.create(vm, structure);
//
// // Add a serializer for custom objects
// const test_fn = JSFunction.create(...); // function that returns true for custom objects
// const serialize_fn = JSFunction.create(...); // function that serializes the object
//
// _ = addSerializer(global, serializers, test_fn, serialize_fn);
//
// // Use the serializer
// const custom_object = ...;
// const result = serialize(global, serializers, custom_object);

View File

@@ -47,6 +47,7 @@ public:
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForJSMockFunction;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForAsyncContextFrame;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForMockWithImplementationCleanupData;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForSnapshotSerializers;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForProcessObject;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForInternalModuleRegistry;
std::unique_ptr<GCClient::IsoSubspace> m_clientSubspaceForErrorCodeCache;

View File

@@ -47,6 +47,7 @@ public:
std::unique_ptr<IsoSubspace> m_subspaceForJSMockFunction;
std::unique_ptr<IsoSubspace> m_subspaceForAsyncContextFrame;
std::unique_ptr<IsoSubspace> m_subspaceForMockWithImplementationCleanupData;
std::unique_ptr<IsoSubspace> m_subspaceForSnapshotSerializers;
std::unique_ptr<IsoSubspace> m_subspaceForProcessObject;
std::unique_ptr<IsoSubspace> m_subspaceForInternalModuleRegistry;
std::unique_ptr<IsoSubspace> m_subspaceForErrorCodeCache;

View File

@@ -17,6 +17,7 @@ pub const Snapshots = struct {
snapshot_dir_path: ?string = null,
inline_snapshots_to_write: *std.AutoArrayHashMap(TestRunner.File.ID, std.array_list.Managed(InlineSnapshotToWrite)),
last_error_snapshot_name: ?[]const u8 = null,
serializers: jsc.Strong.Optional = .empty,
pub const InlineSnapshotToWrite = struct {
line: c_ulong,