mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Implement Bun.{stdin,stderr,stdout} as LazyProperties to prevent multiple instances (#22291)
## Summary
Previously, accessing `Bun.stdin`, `Bun.stderr`, or `Bun.stdout`
multiple times could potentially create multiple instances within the
same thread, which could lead to memory waste and inconsistent behavior.
This PR implements these properties as LazyProperties on
ZigGlobalObject, ensuring:
- ✅ Single instance per stream per thread
- ✅ Thread-safe lazy initialization using JSC's proven LazyProperty
infrastructure
- ✅ Consistent object identity across multiple accesses
- ✅ Maintained functionality as Blob objects
- ✅ Memory efficient - objects only created when first accessed
## Implementation Details
### Changes Made:
- **ZigGlobalObject.h**: Added `LazyPropertyOfGlobalObject<JSObject>`
declarations for `m_bunStdin`, `m_bunStderr`, `m_bunStdout` in the GC
member list
- **BunObject.zig**: Created Zig initializer functions
(`createBunStdin`, `createBunStderr`, `createBunStdout`) with proper C
calling convention
- **BunObject.cpp & ZigGlobalObject.cpp**: Added extern C declarations
and C++ wrapper functions that use
`LazyProperty.getInitializedOnMainThread()`
- **ZigGlobalObject.cpp**: Added `initLater()` calls in constructor to
initialize LazyProperties with lambdas that call the Zig functions
### How It Works:
1. When `Bun.stdin` is first accessed, the LazyProperty initializes by
calling our Zig function
2. `getInitializedOnMainThread()` ensures the property is created only
once per thread
3. Subsequent accesses return the cached instance
4. Each stream (stdin/stderr/stdout) gets its own LazyProperty for
distinct instances
## Test Plan
Added comprehensive test coverage in
`test/regression/issue/stdin_stderr_stdout_lazy_property.test.ts`:
✅ **Multiple accesses return identical objects** - Verifies single
instance per thread
```javascript
const stdin1 = Bun.stdin;
const stdin2 = Bun.stdin;
expect(stdin1).toBe(stdin2); // ✅ Same object instance
```
✅ **Objects are distinct from each other** - Each stream has its own
instance
```javascript
expect(Bun.stdin).not.toBe(Bun.stderr); // ✅ Different objects
```
✅ **Functionality preserved** - Still valid Blob objects with all
expected properties
## Testing Results
All tests pass successfully:
```
bun test v1.2.22 (b93468ca)
3 pass
0 fail
15 expect() calls
Ran 3 tests across 1 file. [2.90s]
```
Manual testing confirms:
- ✅ Multiple property accesses return identical instances
- ✅ Objects maintain full Blob functionality
- ✅ Each stream has distinct identity (stdin ≠ stderr ≠ stdout)
## Backward Compatibility
This change is fully backward compatible:
- Same API surface
- Same object types (Blob instances)
- Same functionality and methods
- Only difference: guaranteed single instance per thread (which is the
desired behavior)
🤖 Generated with [Claude Code](https://claude.ai/code)
---------
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
@@ -72,9 +72,6 @@ pub const BunObject = struct {
|
||||
pub const inspect = toJSLazyPropertyCallback(Bun.getInspect);
|
||||
pub const origin = toJSLazyPropertyCallback(Bun.getOrigin);
|
||||
pub const semver = toJSLazyPropertyCallback(Bun.getSemver);
|
||||
pub const stderr = toJSLazyPropertyCallback(Bun.getStderr);
|
||||
pub const stdin = toJSLazyPropertyCallback(Bun.getStdin);
|
||||
pub const stdout = toJSLazyPropertyCallback(Bun.getStdout);
|
||||
pub const unsafe = toJSLazyPropertyCallback(Bun.getUnsafe);
|
||||
pub const S3Client = toJSLazyPropertyCallback(Bun.getS3ClientConstructor);
|
||||
pub const s3 = toJSLazyPropertyCallback(Bun.getS3DefaultClient);
|
||||
@@ -139,9 +136,6 @@ pub const BunObject = struct {
|
||||
@export(&BunObject.hash, .{ .name = lazyPropertyCallbackName("hash") });
|
||||
@export(&BunObject.inspect, .{ .name = lazyPropertyCallbackName("inspect") });
|
||||
@export(&BunObject.origin, .{ .name = lazyPropertyCallbackName("origin") });
|
||||
@export(&BunObject.stderr, .{ .name = lazyPropertyCallbackName("stderr") });
|
||||
@export(&BunObject.stdin, .{ .name = lazyPropertyCallbackName("stdin") });
|
||||
@export(&BunObject.stdout, .{ .name = lazyPropertyCallbackName("stdout") });
|
||||
@export(&BunObject.unsafe, .{ .name = lazyPropertyCallbackName("unsafe") });
|
||||
@export(&BunObject.semver, .{ .name = lazyPropertyCallbackName("semver") });
|
||||
@export(&BunObject.embeddedFiles, .{ .name = lazyPropertyCallbackName("embeddedFiles") });
|
||||
@@ -188,6 +182,12 @@ pub const BunObject = struct {
|
||||
@export(&BunObject.zstdDecompress, .{ .name = callbackName("zstdDecompress") });
|
||||
// --- Callbacks ---
|
||||
|
||||
// --- LazyProperty initializers ---
|
||||
@export(&createBunStdin, .{ .name = "BunObject__createBunStdin" });
|
||||
@export(&createBunStderr, .{ .name = "BunObject__createBunStderr" });
|
||||
@export(&createBunStdout, .{ .name = "BunObject__createBunStdout" });
|
||||
// --- LazyProperty initializers ---
|
||||
|
||||
// --- Getters ---
|
||||
@export(&BunObject.main, .{ .name = "BunObject_getter_main" });
|
||||
// --- Getters ---
|
||||
@@ -559,39 +559,6 @@ pub fn getOrigin(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue
|
||||
return ZigString.init(VirtualMachine.get().origin.origin).toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn getStdin(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
var rare_data = globalThis.bunVM().rareData();
|
||||
var store = rare_data.stdin();
|
||||
store.ref();
|
||||
var blob = jsc.WebCore.Blob.new(
|
||||
jsc.WebCore.Blob.initWithStore(store, globalThis),
|
||||
);
|
||||
blob.allocator = bun.default_allocator;
|
||||
return blob.toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn getStderr(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
var rare_data = globalThis.bunVM().rareData();
|
||||
var store = rare_data.stderr();
|
||||
store.ref();
|
||||
var blob = jsc.WebCore.Blob.new(
|
||||
jsc.WebCore.Blob.initWithStore(store, globalThis),
|
||||
);
|
||||
blob.allocator = bun.default_allocator;
|
||||
return blob.toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn getStdout(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
var rare_data = globalThis.bunVM().rareData();
|
||||
var store = rare_data.stdout();
|
||||
store.ref();
|
||||
var blob = jsc.WebCore.Blob.new(
|
||||
jsc.WebCore.Blob.initWithStore(store, globalThis),
|
||||
);
|
||||
blob.allocator = bun.default_allocator;
|
||||
return blob.toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn enableANSIColors(globalThis: *jsc.JSGlobalObject, _: *jsc.JSObject) jsc.JSValue {
|
||||
_ = globalThis;
|
||||
return JSValue.jsBoolean(Output.enable_ansi_colors);
|
||||
@@ -2068,6 +2035,40 @@ comptime {
|
||||
|
||||
const string = []const u8;
|
||||
|
||||
// LazyProperty initializers for stdin/stderr/stdout
|
||||
pub fn createBunStdin(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue {
|
||||
var rare_data = globalThis.bunVM().rareData();
|
||||
var store = rare_data.stdin();
|
||||
store.ref();
|
||||
var blob = jsc.WebCore.Blob.new(
|
||||
jsc.WebCore.Blob.initWithStore(store, globalThis),
|
||||
);
|
||||
blob.allocator = bun.default_allocator;
|
||||
return blob.toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn createBunStderr(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue {
|
||||
var rare_data = globalThis.bunVM().rareData();
|
||||
var store = rare_data.stderr();
|
||||
store.ref();
|
||||
var blob = jsc.WebCore.Blob.new(
|
||||
jsc.WebCore.Blob.initWithStore(store, globalThis),
|
||||
);
|
||||
blob.allocator = bun.default_allocator;
|
||||
return blob.toJS(globalThis);
|
||||
}
|
||||
|
||||
pub fn createBunStdout(globalThis: *jsc.JSGlobalObject) callconv(.C) jsc.JSValue {
|
||||
var rare_data = globalThis.bunVM().rareData();
|
||||
var store = rare_data.stdout();
|
||||
store.ref();
|
||||
var blob = jsc.WebCore.Blob.new(
|
||||
jsc.WebCore.Blob.initWithStore(store, globalThis),
|
||||
);
|
||||
blob.allocator = bun.default_allocator;
|
||||
return blob.toJS(globalThis);
|
||||
}
|
||||
|
||||
const Braces = @import("../../shell/braces.zig");
|
||||
const Which = @import("../../which.zig");
|
||||
const options = @import("../../options.zig");
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
macro(origin) \
|
||||
macro(s3) \
|
||||
macro(semver) \
|
||||
macro(stderr) \
|
||||
macro(stdin) \
|
||||
macro(stdout) \
|
||||
macro(unsafe) \
|
||||
macro(valkey) \
|
||||
|
||||
|
||||
@@ -875,6 +875,25 @@ static JSC_DEFINE_CUSTOM_SETTER(setBunObjectMain, (JSC::JSGlobalObject * globalO
|
||||
#define bunObjectReadableStreamToJSONCodeGenerator WebCore::readableStreamReadableStreamToJSONCodeGenerator
|
||||
#define bunObjectReadableStreamToTextCodeGenerator WebCore::readableStreamReadableStreamToTextCodeGenerator
|
||||
|
||||
// LazyProperty wrappers for stdin/stderr/stdout
|
||||
static JSValue BunObject_lazyPropCb_wrap_stdin(VM& vm, JSObject* bunObject)
|
||||
{
|
||||
auto* zigGlobalObject = jsCast<Zig::GlobalObject*>(bunObject->globalObject());
|
||||
return zigGlobalObject->m_bunStdin.getInitializedOnMainThread(zigGlobalObject);
|
||||
}
|
||||
|
||||
static JSValue BunObject_lazyPropCb_wrap_stderr(VM& vm, JSObject* bunObject)
|
||||
{
|
||||
auto* zigGlobalObject = jsCast<Zig::GlobalObject*>(bunObject->globalObject());
|
||||
return zigGlobalObject->m_bunStderr.getInitializedOnMainThread(zigGlobalObject);
|
||||
}
|
||||
|
||||
static JSValue BunObject_lazyPropCb_wrap_stdout(VM& vm, JSObject* bunObject)
|
||||
{
|
||||
auto* zigGlobalObject = jsCast<Zig::GlobalObject*>(bunObject->globalObject());
|
||||
return zigGlobalObject->m_bunStdout.getInitializedOnMainThread(zigGlobalObject);
|
||||
}
|
||||
|
||||
#include "BunObject.lut.h"
|
||||
|
||||
#undef bunObjectReadableStreamToArrayCodeGenerator
|
||||
|
||||
@@ -330,6 +330,11 @@ extern "C" void* Bun__getVM();
|
||||
|
||||
extern "C" void Bun__setDefaultGlobalObject(Zig::GlobalObject* globalObject);
|
||||
|
||||
// Declare the Zig functions for LazyProperty initializers
|
||||
extern "C" JSC::EncodedJSValue BunObject__createBunStdin(JSC::JSGlobalObject*);
|
||||
extern "C" JSC::EncodedJSValue BunObject__createBunStderr(JSC::JSGlobalObject*);
|
||||
extern "C" JSC::EncodedJSValue BunObject__createBunStdout(JSC::JSGlobalObject*);
|
||||
|
||||
static JSValue formatStackTraceToJSValue(JSC::VM& vm, Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSObject* errorObject, JSC::JSArray* callSites)
|
||||
{
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
@@ -3463,6 +3468,17 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(JSC::JSBigInt64Array::create(init.owner, JSC::JSBigInt64Array::createStructure(init.vm, init.owner, init.owner->objectPrototype()), 7));
|
||||
});
|
||||
|
||||
// Initialize LazyProperties for stdin/stderr/stdout
|
||||
m_bunStdin.initLater([](const LazyProperty<JSC::JSGlobalObject, JSC::JSObject>::Initializer& init) {
|
||||
init.set(JSC::JSValue::decode(BunObject__createBunStdin(init.owner)).getObject());
|
||||
});
|
||||
m_bunStderr.initLater([](const LazyProperty<JSC::JSGlobalObject, JSC::JSObject>::Initializer& init) {
|
||||
init.set(JSC::JSValue::decode(BunObject__createBunStderr(init.owner)).getObject());
|
||||
});
|
||||
m_bunStdout.initLater([](const LazyProperty<JSC::JSGlobalObject, JSC::JSObject>::Initializer& init) {
|
||||
init.set(JSC::JSValue::decode(BunObject__createBunStdout(init.owner)).getObject());
|
||||
});
|
||||
|
||||
configureNodeVM(vm, this);
|
||||
|
||||
#if ENABLE(REMOTE_INSPECTOR)
|
||||
|
||||
@@ -621,6 +621,10 @@ public:
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSBunRequestStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSObject>, m_JSBunRequestParamsPrototype) \
|
||||
\
|
||||
V(public, LazyPropertyOfGlobalObject<JSObject>, m_bunStdin) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSObject>, m_bunStderr) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSObject>, m_bunStdout) \
|
||||
\
|
||||
V(public, LazyPropertyOfGlobalObject<Structure>, m_JSNodeHTTPServerSocketStructure) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSFloat64Array>, m_statValues) \
|
||||
V(public, LazyPropertyOfGlobalObject<JSBigInt64Array>, m_bigintStatValues) \
|
||||
@@ -654,6 +658,11 @@ public:
|
||||
|
||||
JSObject* nodeErrorCache() const { return m_nodeErrorCache.getInitializedOnMainThread(this); }
|
||||
|
||||
// LazyProperty accessors for stdin/stderr/stdout
|
||||
JSC::JSObject* bunStdin() const { return m_bunStdin.getInitializedOnMainThread(this); }
|
||||
JSC::JSObject* bunStderr() const { return m_bunStderr.getInitializedOnMainThread(this); }
|
||||
JSC::JSObject* bunStdout() const { return m_bunStdout.getInitializedOnMainThread(this); }
|
||||
|
||||
Structure* memoryFootprintStructure()
|
||||
{
|
||||
return m_memoryFootprintStructure.getInitializedOnMainThread(this);
|
||||
|
||||
Reference in New Issue
Block a user