- Phase A는 Zig 파일 하나를 같은 디렉터리의 초안 .rs로 옮기는 단계이며, 컴파일 가능성보다 Zig와 나란히 비교 가능한 동일 논리와 구조 보존을 우선함
- Bun은 자체 이벤트 루프와 syscall을 쓰기 때문에 tokio, rayon, hyper, futures, std::fs, std::net, std::process를 금지하고, async fn 대신 Zig와 같은 콜백·상태 머신을 유지함
- import와 타입은 bun_<area> 크레이트 체계로 옮기며, bun.String은 bun_str, bun.sys는 bun_sys, JSC.JSValue와 JSGlobalObject는 bun_jsc 계열로 매핑됨
- 문자열·경로·HTTP·환경 변수 같은 외부 데이터는 str이 아니라 바이트로 다루며, AST/parser 계열은 arena를 유지하고 그 외 영역은 전역 mimalloc 기반 Box·Vec 중심으로 allocator 인자를 제거함
- 포팅 중 확신할 수 없는 변환은 // TODO(port), 성능 관용구 변화는 // PERF(port), unsafe 블록은 // SAFETY:로 남기며, Box::leak, lifetime 회피용 raw pointer, todo!() 같은 금지 패턴은 작성하지 않음
포팅 단계의 목표와 공통 원칙
- Phase A의 목표는 Zig 파일 하나를 Rust로 옮겨 같은 디렉터리의 .zig 옆에 동일한 논리를 담은 초안 .rs를 만드는 것임
- 초안은 컴파일될 필요가 없고, Phase B에서 크레이트별로 컴파일 가능하게 만듦
- .rs 작성 전 문서 전체를 읽어야 함
- 파일 배치는 기존 Zig 구조를 따름
- <area>는 항상 src/ 아래 첫 번째 경로 컴포넌트이며 크레이트 루트가 됨
- .zig와 같은 basename을 쓰되, basename이 즉시 상위 디렉터리명과 같으면 mod.rs, 최상위 <area> 디렉터리명과 같으면 lib.rs를 사용함
- 예: src/bake/DevServer/HmrSocket.zig → src/bake/DevServer/HmrSocket.rs
- 예: src/bake/DevServer/DevServer.zig → src/bake/DevServer/mod.rs
- 예: src/http/http.zig → src/http/lib.rs
- 새 크레이트 레이아웃은 만들지 않음
- 영역 간 타입은 bun_<area>::Type 형태로 참조함
- Cargo.toml 연결은 Phase B에서 처리함
- Bun은 자체 이벤트 루프와 syscall을 사용하므로 Rust의 I/O 관련 모듈과 특정 런타임·라이브러리를 금지함
- 금지: tokio, rayon, hyper, async-trait, futures
- 금지: std::fs, std::net, std::process
- 허용: Rust core/std의 slice, iter, mem, fmt, core::ffi
- async fn은 사용하지 않고 Zig와 동일하게 콜백과 상태 머신으로 작성함
- Zig에서 이미 unsafe였던 부분은 Rust에서도 unsafe를 사용할 수 있음
- 모든 unsafe 블록에는 // SAFETY: <why>를 붙여 Zig의 불변식을 반영해야 함
- 확신할 수 없는 변환은 추측하지 않고 // TODO(port): <reason>을 남김
- 잘못된 코드보다 플래그를 남기는 편이 낫다고 규정함
- Zig의 성능 특화 관용구를 평범한 Rust 관용구로 옮긴 곳에는 // PERF(port): <zig idiom> — profile in Phase B를 남김
- 대상 예: appendAssumeCapacity, arena bulk-free, stack-fallback alloc, comptime monomorphization
- Phase A는 정확성과 관용구를 우선하고, Phase B에서 PERF(port)를 찾아 벤치마크함
- Zig 구조를 최대한 맞춰 Phase B 리뷰어가 .zig와 .rs를 나란히 비교할 수 있어야 함
- 함수 이름은 같은 이름을 snake_case로 유지함
- 필드 순서와 제어 흐름도 유지함
- 약어는 하나의 소문자 단어로 접음: toAPI→to_api, isCSS→is_css, toUTF8→to_utf8, toJS→to_js, errorInCI→error_in_ci
- 2개 이상 연속 대문자는 하나의 세그먼트로 취급함
- out-param 생성자 예외가 있음
- fn foo(this: *@This(), ...) !void가 this.* = .{...}를 대입하는 형태면 Rust에서는 fn foo(...) -> Result<Self, E>로 바꿈
- Zig는 error union에 보장된 NRVO가 없어 out-param을 쓰지만 Rust는 그렇지 않음
- this가 풀이나 배열의 사전 할당 슬롯이라 in-place init이 필요한 경우 &mut MaybeUninit<Self>를 유지하고 // TODO(port): in-place init을 남김
- pub fn deinit은 같은 이름의 inherent method가 아니라 impl Drop으로 변환함
- borrow checker 때문에 흐름을 재구성할 수 있음
- Zig 흐름을 그대로 옮기면 겹치는 &mut가 생길 때 .len()이나 index 같은 scalar 값을 local에 저장하고 borrow를 끊은 뒤 다시 borrow함
- borrow checker를 피하려고 raw pointer를 쓰지 않음
- 이런 재구성에는 // PORT NOTE: reshaped for borrowck를 남김
- 모든 크레이트의 전제 조건으로 binary root에 전역 allocator가 필요함
- #[global_allocator] static ALLOC: bun_alloc::Mimalloc = bun_alloc::Mimalloc;
- 이 설정이 Box/Rc/Arc/Vec 매핑 전에 있어야 하며, 없으면 mimalloc이 아니라 glibc malloc으로 조용히 바뀜
- Phase B가 이를 검증하고 Phase A는 가정할 수 있음
크레이트와 import 매핑
- @import("bun").X는 매핑 표에서 X를 찾아 옮김
- @import("../<area>/file.zig")는 bun_<area>::file::Thing으로 매핑함
- 표에 없으면 크레이트는 bun_<top>임
- <top>은 src/ 아래 첫 번째 디렉터리명을 그대로 사용함
- 예: crash_handler → bun_crash_handler
- bun_alloc은 그대로 bun_alloc이며 이중 prefix를 붙이지 않음
- 중간 디렉터리는 snake_case 모듈 경로가 됨
- 예: src/bake/DevServer/Assets.zig → bun_bake::dev_server::Assets
- 주요 네임스페이스 매핑은 다음을 따름
- bun.String, bun.strings, ZigString → bun_str
- bun.sys, bun.FD, Maybe(T) → bun_sys
- bun.jsc, JSValue, JSGlobalObject, CallFrame, JSRef, Strong → bun_jsc
- bun.uws, us_socket_t, Loop → raw는 bun_uws_sys, wrapper는 bun_uws
- bun.Output, bun.Global, bun.fmt, bun.env_var → bun_core
- bun.allocators, MimallocArena, bun.default_allocator → bun_alloc
- bun.http → bun_http
- bun.Async, FilePoll, KeepAlive → bun_aio
- bun.threading, ThreadPool → bun_threading
- bun.jsc.WorkPool → bun_threading::WorkPool
- bun.logger → bun_logger
- bun.ast, js_parser, js_lexer, Expr, Stmt → bun_js_parser
- bun.ImportRecord, bun.ImportKind의 src/options_types/ → bun_options_types
- bun.options, bun.options.Loader의 src/bundler/options.zig → bun_bundler::options
- bun.Semver → bun_semver
- bun.glob → bun_glob
- bun.path, resolve_path → bun_paths
- bun.BoringSSL → bun_boringssl 및 bun_boringssl_sys
- bun.shell → bun_shell
- bun.bake → bun_bake
- bun.install → bun_install
- bun.bundle_v2, Transpiler → bun_bundler
- 경로와 해시 관련 매핑은 별도 주의가 필요함
- bun.PathBuffer, bun.WPathBuffer, bun.OSPathBuffer, bun.MAX_PATH_BYTES, bun.path_buffer_pool, bun.w_path_buffer_pool → bun_paths
- bun_paths::PathBuffer는 [u8; MAX_PATH_BYTES]
- bun_paths::path_buffer_pool()은 RAII guard를 반환함
- std.fs.path.sep / sep_str / delimiter / isAbsolute → bun_paths
- bun_paths::SEP: u8, SEP_STR: &str, DELIMITER: u8, is_absolute(&[u8])
- std::path는 OsStr 기반이고 타입이 맞지 않으므로 사용하지 않음
- bun.hash(...) → bun_wyhash::hash, seed 0의 std.hash.Wyhash wrapper이며 Wyhash11이 아님
- bun.Wyhash11 → bun_wyhash::Wyhash11
- bun.ptr.*의 Owned, Shared, AtomicShared, RefCount, TaggedPointer, WeakPtr는 std 또는 bun_collections로 매핑함
- std.ArrayList, std.AutoHashMap, MultiArrayList, BabyList는 bun_collections 또는 std로 매핑하며, 자세한 기준은 Collections 규칙을 따름
타입 매핑
-
슬라이스와 문자열
- c_int, c_char, c_void는 prelude에 없으며 core::ffi::*에서 가져옴
- []const u8는 함수 파라미터·반환값이면 &[u8]로 변환함
- []const u8가 struct field면 같은 파일의 deinit을 보고 결정함
- allocator.free(self.field)가 있으면 Box<[u8]>, 증가하면 Vec<u8>
- 절대 free되지 않고 literal만 대입되면 &'static [u8]
- arena-owned인 CSS/parser 계열이면 raw *const [u8] 또는 StoreRef
- 같은 기준을 일반 []const T에도 적용함
- Phase A에서는 struct에 lifetime param을 넣지 말고 Box vs &'static vs raw를 결정함
- []u8 → &mut [u8]
- [:0]const u8 → &ZStr (bun_str::ZStr)
- [:0]u8 → &mut ZStr
- [:0]const u16 → &bun_str::WStr
- [:0]u16 → &mut bun_str::WStr
- [*:0]const u8는 extern "C" signature와 #[repr(C)] field에서는 *const c_char, Rust 내부 함수 파라미터·반환값에서는 &CStr
- FFI 경계에서는 CStr::from_ptr로 변환함
-
포인터와 optional
- ?T → Option<T>
- ?*T / *T / *const T가 struct field인 경우 docs/LIFETIMES.tsv를 조회함
- 컬럼은 file·struct·field·zig_type·class·rust_type·evidence
- rust_type 컬럼을 그대로 사용하며, cross-file 분석으로 미리 계산돼 있으므로 로컬 추측보다 신뢰함
- OWNED → Box<T>
- SHARED → Rc/Arc<T>
- BORROW_PARAM → &'a T, struct에는 <'a> 추가
- STATIC → &'static T
- JSC_BORROW → &JSGlobalObject 등
- BACKREF / INTRUSIVE / FFI → raw *const / *mut T
- ARENA → &'bump T
- UNKNOWN → Option<NonNull<T>>와 // TODO(port): lifetime
- ?*T / *T / *const T가 field가 아닌 함수 파라미터·반환값이면 Option<&T> / &mut T / &T
- raw pointer는 extern "C" 경계에서만 사용함
- anyopaque → core::ffi::c_void
-
오류 타입과 Result
- anyerror!T → Result<T, bun_core::Error>
- Phase A에서는 항상 이 매핑을 사용하고, Phase B에서 call graph가 허용하는 경우 local enum으로 좁힘
- bun_core::Error는 enum이 아니라 #[repr(transparent)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Error(NonZeroU16)
- link-time으로 등록된 name table을 사용하며, bun_core::err!("ENOENT")는 tag를 intern하고 const Error를 산출함
- .name() -> &'static str는 정확한 Zig tag를 반환함
- crate별 thiserror enum은 Into<bun_core::Error>를 자동 derive함
- anyhow::Error나 Box<dyn Error>는 절대 사용하지 않음
- heap allocation, !Copy, @errorName snapshot 호환성, bare error를 저장하는 77개 struct field를 깨뜨림
- !T inferred error set → Result<T, bun_core::Error>이며 // TODO(port): narrow error set을 남김
- 예외적으로 body의 try가 allocation뿐이면 Result<T, bun_alloc::AllocError>를 직접 사용함
- bare anyerror 값은 bun_core::Error
- OOM!T, bun.OOM!T, error{OutOfMemory}!T → Result<T, bun_alloc::AllocError>
- bun.JSOOM!T → bun_jsc::JsResult<T>
- error{A,B}!T → Result<T, FooError>와 #[derive(thiserror::Error, strum::IntoStaticStr)] enum FooError { A, B }
- bun.JSError!T → bun_jsc::JsResult<T>
- Maybe(T) (bun.sys) → bun_sys::Result<T>
-
JSC와 Bun 타입
- JSC.JSValue → bun_jsc::JSValue, #[repr(transparent)] i64, Copy, !Send
- *JSC.JSGlobalObject → &bun_jsc::JSGlobalObject, 항상 borrow이며 owned가 아님
- JSC.CallFrame → &bun_jsc::CallFrame
- bun.String → bun_str::String
- bun.PathBuffer ([MAX_PATH_BYTES]u8) → bun_paths::PathBuffer
- var buf: bun.PathBuffer = undefined; → let mut buf = bun_paths::PathBuffer::uninit();
- bun.WPathBuffer → bun_paths::WPathBuffer
- std.mem.Allocator → &dyn bun_alloc::Allocator
-
기본 타입, 구조체, enum, FFI handle
- u32, i64, usize, c_int, bool은 Rust의 동일 타입으로 1:1 매핑함
- packed struct(uN)은 모든 field가 bool이면 bitflags!
- 그렇지 않으면 #[repr(transparent)] pub struct Foo(uN)와 field 순서에 맞춘 manual const/shift accessor를 사용함
- enum(uN) → #[repr(uN)] enum
- union(enum) → payload variant가 있는 Rust enum
- extern struct → #[repr(C)] struct
- pub const Foo = opaque {};처럼 FFI handle이 *Foo로 쓰이면 Nomicon 패턴을 사용함
- opaque {}가 GenericIndex(u32, opaque {}) 같은 type-tag로 쓰이면 제거하고 pub struct FooId(u32); 같은 newtype을 선언함
-
anytype과 comptime type 파라미터
- x: anytype은 단일 trait로 덮을 수 있으면 x: impl Trait
- 단일 trait가 없으면 body가 실제 호출하는 method에 맞춘 generic <T>를 사용함
- body가 x의 method를 전혀 호출하지 않는 opaque context/userdata 패턴이면 bound 없는 <C>를 사용함
- 호출 간 저장되면 <C: 'static>을 사용하고, C를 통해 *mut c_void로 왕복하면 Box::into_raw를 사용함
- printf-style 함수의 args: anytype은 format_args!를 통한 core::fmt::Arguments
- (comptime X: type, arg: X) 쌍은 type param을 없애고 arg: &mut impl Trait로 작성함
- writer는 텍스트면 &mut impl core::fmt::Write, bytes면 &mut impl bun_io::Write
관용구와 내장 함수 매핑
-
deinit, Drop, allocator free
- defer x.deinit()은 삭제하고 impl Drop for T가 scope exit에서 암묵 처리함
- ManuallyDrop<T>는 arena-allocated 값이 arena.reset()으로 해제되는 경우, destruction order가 correctness에 중요할 때, .classes.ts class의 m_ctx payload로 finalize()가 teardown을 소유할 때처럼 제한된 경우에만 사용함
- 명시적 early release가 필요하면 public API로 deinit(&mut self)를 노출하지 않고, socket이나 fd는 ownership을 받는 close(self)로 이름 붙임
- pub fn deinit(self: *T) 정의는 impl Drop for T로 변환함
- body가 owned field를 free/deinit만 하면 body 전체를 삭제하고, Rust가 Box/Vec field를 자동 drop함
- FD close, intrusive refcount deref, FFI destroy call 같은 side effect가 있을 때만 명시적 Drop body를 유지함
- deinit이 allocator param을 받으면 field가 allocator를 소유하도록 Box/Vec으로 재타입 지정함
- #[repr(C)]이고 FFI를 건너 생성·파괴되는 타입은 unsafe fn destroy(*mut Self)를 유지함
- .classes.ts payload는 Drop이 아니라 finalize를 사용함
- allocator.free(this.field) / allocator.free(local)은 삭제하고 field/local을 Box<[T]> 또는 Vec<T>로 재타입 지정함
- default allocator가 아닌 allocator와 맞춰야 하는 allocation만 명시적 alloc.dealloc(ptr, layout)을 유지함
- defer pool.put(x) after pool.get()은 Rust pool guard로 대체함
-
errdefer와 cleanup
- errdefer x.deinit() / errdefer alloc.free(x)는 새로 만든 local이면 삭제함
- Vec/Box/Drop type이 되면 ? 오류 경로에서 자동 drop됨
- side effect가 있는 errdefer { ... }는 scopeguard::guard(state, |s| <cleanup>)를 사용함
- 성공 경로에서 ScopeGuard::into_inner(guard)로 disarm함
- 직접 Drop struct와 mem::forget을 만들지 않음
- cleanup이 서로 다른 &mut borrow 2개 이상을 capture해 scopeguard로 표현할 수 없을 때만 // TODO(port): errdefer를 남김
-
comptime과 generic 변환
- comptime T: type param → plain generic <T>이며, body가 호출하는 method에 맞춰 trait bound를 추가함
- Rust const generic은 값용이지 타입용이 아니므로 타입용으로 쓰지 않음
- comptime flag: bool / comptime n: uN → <const FLAG: bool> / <const N: uN>
- param이 forward만 되고 type/const position에서 쓰이지 않으면 runtime arg로 낮추고 // PERF(port): was comptime monomorphization — profile in Phase B를 남김
- hot inner-loop branch를 가르는 bool은 낮추지 않음
- 예: printer의 enable_ansi_colors, NewHTTPContext의 ssl
- comptime e: SomeEnum → <const E: SomeEnum>이며 enum에는 #[derive(core::marker::ConstParamTy, PartialEq, Eq)]를 붙임
- expression의 comptime은 const fn 또는 const { }
- Zig가 token-pasting이나 공유 trait 없는 type-list iteration을 할 때만 macro_rules!를 사용함
- fn Foo(comptime T: type[, comptime opts...]) type { return struct {...} }는 pub struct Foo<T[, const OPTS...]> { ... }와 impl<T> Foo<T> { ... }로 변환함
-
switch, union, bool dispatch
- switch (u) { inline else => |v[, tag]| v.expr() }는 variant별 match를 전개함
- payload type들이 호출 method를 공유하면 그 method는 trait에 속해야 함
- switch (b) { inline else => |c| callee(c, ...) }에서 callee가 <const B: bool>을 계속 필요로 하면 if b { callee::<true>(...) } else { callee::<false>(...) }
- comptime bool이 forward만 되고 type position에 쓰이지 않으면 const param을 제거하고 runtime bool을 넘기며 // PERF(port): was comptime bool dispatch — profile in Phase B를 남김
- tagged union에 대한 switch는 match로 변환함
-
기본값, labeled block, @This
- struct field default가 field: T = .{} / = "" / = 0이고 모든 default가 field type의 Default와 같으면 #[derive(Default)]
- 그렇지 않으면 impl Default for T { fn default() -> Self { ... } }
- callsite의 .{}는 T::default()
- owned slice field의 = ""에 대한 Default는 empty slice인 Box::default()
- std.fmt.comptimePrint는 literal 연결이면 concat!(...), 정적 문자열 formatting이면 const_format::formatcp!(...)
- Zig가 zero cost로 처리한 위치에서 runtime heap allocation을 만드는 format!은 절대 사용하지 않음
- const x = brk: { ...; break :brk v; }는 Rust stable 1.65의 labeled block으로 변환함
- block이 40줄을 넘고 break point가 3개 이상일 때만 helper로 끌어내고 // TODO(port): hoisted from labeled block를 남김
- file-level const Foo = @This();는 삭제하고 pub struct Foo { … } 이름을 직접 사용함
- generic fn body 안의 @This()는 Self
-
cast와 pointer 변환
- @as(T, x)는 보통 삭제하고 Rust의 타입 추론에 맡김
- nested cast의 result type을 정하는 용도면 cast 자체에 target type을 씀
- @as(u32, @intCast(x)) → u32::try_from(x).unwrap() 또는 x as u32
- @fieldParentPtr("field", ptr)는 offset_of!를 이용해 parent pointer를 복원하고 // SAFETY: ptr points to Parent.field를 붙임
- @ptrCast / @alignCast → ptr.cast::<T>() / &*(p as *const T) in unsafe
- @intFromEnum(e) → e as uN
- @enumFromInt(n) → unsafe { core::mem::transmute::<uN, E>(n) } 또는 range를 debug-assert하는 const fn E::from_raw(n: uN) -> E
- hot path에서 FromPrimitive는 variant 전체에 대한 runtime match를 만들기 때문에 절대 사용하지 않음
- @intCast(x)는 narrowing이면 T::try_from(x).unwrap()로 항상 checked 처리함
- Phase B가 증명된 hot loop에서 // PERF(port): @intCast와 함께 as로 바꿀 수 있음
- widening은 x.into() 또는 T::from(x)
- @truncate(x) → x as T
- @intFromBool(b) → b as uN 또는 usize::from(b)
- @floatFromInt(x) → x as f64
- @intFromFloat(x) → x as uN
- Rust는 overflow/NaN에서 saturate하지만 Zig는 UB이므로, Zig가 사전 range check에 의존했다면 유지하고 새 check를 추가하지 않음
- @bitCast(x)는 같은 크기의 POD에 unsafe { core::mem::transmute(x) }
- 안전한 대안이 있으면 f64::to_bits/from_bits, u32::from_ne_bytes, packed-struct .bits()를 선호함
- @intFromPtr(p) → p as usize 또는 strict-provenance의 p.addr()
- @ptrFromInt(n) → n as *mut T in unsafe
- 실제 pointer를 round-trip하는 경우 provenance 보존을 위해 ptr.byte_add(off)를 선호함
-
메모리와 산술 내장
- @memcpy(dst, src) → dst.copy_from_slice(src)
- length mismatch에서 panic하며 Zig와 같고, non-overlapping 전용임
- bun.copy(T, dst, src) → dst[..src.len()].copy_from_slice(src)
- 같은 buffer라서 overlap 가능하면 dst.copy_within(range, dest_idx) 또는 unsafe { core::ptr::copy(src.as_ptr(), dst.as_mut_ptr(), src.len()) }
- @memset(dst, v) → dst.fill(v)
- raw bytes zeroing은 unsafe { ptr::write_bytes(p, 0, n) }
- @min(a, b) / @max(a, b) → a.min(b) / a.max(b)
- @tagName(e) → <&'static str>::from(e) 또는 e.into()
- @errorName(e) → <&'static str>::from(e)
- Display/to_string()은 human message라 Zig output과 달라지므로 사용하지 않음
- snapshot tests, JS error.code, crash-handler trace encoding이 정확한 문자열에 의존함
- saturating 연산은 a -| b → a.saturating_sub(b), a +| b → a.saturating_add(b), a *| b → a.saturating_mul(b)
- wrapping 연산은 a +% b → a.wrapping_add(b), a -% b → a.wrapping_sub(b), a *% b → a.wrapping_mul(b)
- Rust는 debug에서 panic하므로 bare +를 쓰지 않음
- std.math.maxInt(T) / std.math.minInt(T) → T::MAX / T::MIN
- std.mem.zeroes(T)는 #[repr(C)] POD이고 NonNull/NonZero/enum field가 없을 때만 unsafe { core::mem::zeroed::<T>() }
- 그 외에는 T::ZEROED나 Default를 직접 구현함
-
문자열·반복·옵션 관용구
- std.mem.span(p) on [*:0]const u8 → unsafe { CStr::from_ptr(p) }.to_bytes() 또는 bun_str::ZStr::from_ptr(p)
- std.mem.sliceTo(buf, 0) → &buf[..buf.iter().position(|&b| b == 0).unwrap()] 또는 bun_str::slice_to_nul(buf)
- inline for over tuple은 모든 요소 타입이 같으면 const [T; N]와 일반 for
- heterogeneous type일 때만 macro_rules!나 unrolling을 사용함
- for (slice, 0..) |x, i| → for (i, x) in slice.iter().enumerate()
- for (a, b) |x, y| → for (x, y) in a.iter().zip(b)이며, zip의 조용한 truncation을 막기 위해 debug_assert_eq!(a.len(), b.len())를 추가함
- try x → x?
- orelse → .unwrap_or(..) / .ok_or(..)? / let Some(x) = .. else { .. }
- if (x) |y| → if let Some(y) = x
- while (it.next()) |x| → while let Some(x) = it.next() 또는 for x in it
- std.mem.tokenizeScalar(u8, s, c) → s.split(|b| *b == c).filter(|s| !s.is_empty())
- std.mem.trimRight(u8, s, chars) → bun_str::strings::trim_right(s: &[u8], chars: &[u8]) -> &[u8]
- bun.strings.w("...") → bun_str::w!("...")
- bun.strings.fooComptime(x, "lit") → bun_str::strings::foo(x, b"lit")
-
assert, logging, thread-local, formatting
- bun.assert(x) → debug_assert!(x)
- comptime bun.assert(x) → item scope의 const _: () = assert!(x);
- bun.unreachablePanic(...) / unreachable → unreachable!()
- @branchHint(.cold)는 함수에 #[cold]를 붙이거나 cold path용 nested function을 사용함
- bun.Output.scoped(.X, .vis)("fmt", .{a,b}) → bun_output::scoped_log!(X, "fmt {} {}", a, b);
- visibility는 module level의 bun_output::declare_scope!(X, hidden);로 한 번 등록해 인코딩함
- Zig {s}가 []const u8에 쓰인 경우 인자를 bstr::BStr::new(x)로 감쌈
- bytes가 valid UTF-8이 아닐 수 있으므로 from_utf8를 쓰지 않음
- scoped_log!는 반드시 if cfg!(feature="debug_logs") && SCOPE.enabled() { ... }로 확장돼야 함
- release에서 보간 표현식 평가를 강제하지 않도록 gate 밖에서 format_args!를 미리 만들지 않음
- threadlocal var X: T = init; → thread_local! { static X: Cell<T> = const { Cell::new(init) }; }
- 큰 buffer의 thread-local은 thread_local! { static BUF: RefCell<PathBuffer> = const { RefCell::new(PathBuffer::ZEROED) }; }
- pub fn format(self, writer: *std.Io.Writer) !void는 impl core::fmt::Display for T로 변환함
컴파일타임 리플렉션
- param: anytype에 대한 @TypeOf(param)은 제거하고, 제네릭을 <T>로 이름 붙여 T를 직접 사용함
- Zig는 anytype에 이름이 없어 @TypeOf가 필요하지만, Rust에서는 제네릭 매개변수 자체가 이름이 됨
- @TypeOf가 @typeInfo에 전달되는 진짜 리플렉션일 때만 별도 처리가 필요함
- @typeInfo(T)와 @field(x, "name")에는 Rust 등가물이 없음
- 구조체 필드를 순회해 equality/hash/clone/drop을 구현했다면 #[derive(PartialEq, Eq, Hash, Clone)]을 쓰고, Drop은 직접 구현함
- 필드 순회가 toCss, parse, toJS 같은 도메인 프로토콜 구현 목적이면 프로토콜을 trait로 만들고 타입별로 구현함
- 런타임에 필드 이름이 정말 필요할 때만 범용 Fields 리플렉션 derive를 사용함
- @hasDecl(T, "foo")는 런타임 검사로 옮기지 않음
- if (@hasDecl(T, "foo")) T.foo(x) else @compileError(...)는 T: Foo trait bound와 x.foo() 호출로 바꿈
- 선택적 동작인 else default_expr는 기본 메서드가 있는 trait 또는 타입이 override할 수 있는 blanket impl로 처리함
- 함수 시그니처를 검사하는 host_fn 패턴은 proc-macro attribute로 처리하고 // TODO(port): proc-macro를 남김
- intrusive list에서 쓰는 @field(x, comptime name)은 Rust 1.77부터 안정화된 core::mem::offset_of!(T, field)로 raw pointer offset을 유지함
문자열
- 데이터는 str이 아니라 바이트로 다룸
- 파일 경로, 소스 코드, HTTP 바이트, 모듈 specifier, 환경 변수, syscall이나 네트워크에서 온 데이터에는 std::string::String, &str, .to_string(), String::from_utf8*를 쓰지 않음
- 해당 데이터는 &[u8], Vec<u8>, Box<[u8]>로 표현함
- Bun은 WTF-8과 임의 바이트를 처리하므로 UTF-8 검증을 넣으면 성능 비용이 생기고, 유효한 Linux 경로나 lone surrogate를 거부하는 정확성 버그가 됨
- &str/String 사용은 제한됨
- 직접 쓴 문자열 리터럴 또는 Rust API가 정말 &str을 요구하는 마지막 단계에서만 사용함
- Display/Debug에는 from_utf8_lossy 대신 bstr::BStr::new(bytes)를 사용함
- 외부 데이터에 대해 from_utf8(...).unwrap()을 쓰지 않음
- Zig 문자열/바이트 조작은 Rust 바이트 API와 Bun 전용 SIMD 경로로 옮김
- []const u8의 text-ish 데이터 → &[u8], &str 아님
- 성장하는 소유 text buffer → Vec<u8>, String 아님
- std.mem.eql(u8, a, b) → a == b
- bun.strings.eqlComptime(a, "lit") → a == b"lit"
- bun.strings.hasPrefix / hasSuffix → a.starts_with(p) / .ends_with(p)
- bun.strings.indexOfChar(a, c) / indexOfScalar → bun_str::strings::index_of_char(a, c), FFI로 highway_index_of_char SIMD를 사용하며 memchr/bstr로 대체하지 않음
- bun.strings.indexOf(a, n) → bun_str::strings::index_of(a, n), highway SIMD substring 사용
- bun.strings.indexOfAny(a, set) / indexOfAnyT → bun_str::strings::index_of_any(a, set), FFI로 highway_index_of_any_char 사용
- bun.strings.containsChar / contains → bun_str::strings::index_of_char(..).is_some()
- bun.highway.* → bun_highway::*, 동일 C++의 direct extern "C" re-export
- 목록에 없는 다른 bun.strings.<fn>은 bun_str::strings::<fn>로 옮기고, src/string/immutable.zig를 1:1 포팅하며 hot-path scanner를 bstr/memchr로 대체하지 않음
- cold-path byte op 중 bun.strings 등가물이 없는 .split(), .trim_ascii(), 임시 .find()에는 bstr::ByteSlice extension trait 사용 가능함
- std.fmt.allocPrint(a, "..", .{}) → Vec<u8>에 use std::io::Write; write!(&mut v, ..)로 작성하고 allocator를 제거하며 String을 반환하는 format!은 쓰지 않음
- std.fmt.bufPrint(buf, ..) → write!(&mut &mut buf[..], ..)
- 공유/ref-counted 문자열은 공유 상태를 유지함
- bun.String은 WTFString 기반 공유 버퍼이며 JSC로 복사 없이 넘어가므로 bun_str::String으로 유지함
- Arc<str>나 Rust String으로 단순화하면 zero-copy JS interop과 Latin-1/UTF-16 저장을 잃음
- Rust에서 bun_str::String은 #[repr(C)] struct { tag: u8, value: StringValue } 형태이며, C++가 FFI 너머에서 tag와 value를 독립적으로 변경하므로 Rust enum으로 만들지 않음
- bun.String 관련 변환 규칙이 정해짐
- s.toUTF8(alloc) → s.to_utf8()
- 반환값은 bun_str::Utf8Slice<'_>이며 이미 UTF-8이면 borrow하고 아니면 transcode buffer를 소유하며 Drop에서 해제함
- to_utf8()에는 allocator 인자가 없고, 검증이 아니라 WTF-16→UTF-8 인코딩이며 출력은 바이트임
- s.toJS(global) → s.to_js(global), StringJsc extension trait을 통해 *_jsc/runtime/jsc crate에서만 호출 가능함
- base crate에서 .toJS를 호출하면 // TODO(port): move to *_jsc를 남김
- bun.String.borrowUTF8(slice) → bun_str::String::borrow_utf8(slice), 호출자가 slice 생존을 유지하며 borrow에는 'a lifetime이 붙음
- ZigString → bun_str::ZigString, legacy이며 bun_str::String을 선호함
- NUL 종료 문자열은 [:0]const u8에서 &ZStr로 옮김
- ZStr<'a>는 ptr: *const u8, len: usize, _p: PhantomData<&'a [u8]>를 가지며 .as_bytes(), .as_ptr(), .as_cstr()를 제공함
- len에는 NUL이 포함되지 않음
- 방금 NUL 종료한 버퍼에서는 unsafe { ZStr::from_raw(buf.as_ptr(), len) }를 사용하고, // SAFETY: buf[len] == 0 written above 같은 안전성 조건을 남김
- [:0]u16에는 WStr::from_raw 또는 WStr::from_raw_mut(buf.as_mut_ptr(), len)을 사용하고, ZStr::from_raw_mut도 동일하게 적용함
할당자와 arena
- AST/parser crate는 arena를 유지하고, 나머지는 global allocator를 사용함
- AST crate는 js_parser, js_printer, css, bundler, bake, sourcemap, shell parser, interchange, install/lockfile임
- 이 crate들은 작은 노드로 된 큰 tree를 만들고 parse 종료 시 bulk-free하므로 arena allocation이 처리량에 중요함
- AST crate에서는 arena와 lifetime을 명시적으로 전달함
- MimallocArena / std.heap.ArenaAllocator → bumpalo::Bump, bun_alloc::Arena로 re-export됨
- 호출자가 arena를 넘기는 std.mem.Allocator param → bump: &'bump Bump로 바꾸고 struct/fn에는 <'bump> lifetime을 붙임
- 호출자가 bun.default_allocator를 넘기면 param을 삭제하고 global mimalloc을 사용함
- arena를 쓰는 std.ArrayList(T) / ArrayListUnmanaged(T) → bumpalo::collections::Vec<'bump, T>
- .append(a, x) → v.push(x), arena는 생성 시점에 묶이고 호출마다 전달하지 않음
- arena의 allocator.create(T) → bump.alloc(init)이며 &'bump mut T 반환
- allocator.dupe(u8, s) → bump.alloc_slice_copy(s)이며 &'bump [u8] 반환
- arena.reset() → bump.reset(), 모든 'bump 값이 무효화되고 borrow checker가 이를 강제함
- Expr.Data.Store, Stmt.Data.Store, ASTMemoryAllocator는 stable address가 필요한 typed slab이므로 typed_arena::Arena<T>를 사용함
- node 간 참조는 &'arena Expr이고 Vec<Expr>로 바꾸지 않음
- AST 외 crate에서는 allocator 인자를 제거함
- std.mem.Allocator param은 삭제하며 Box/Vec/String은 global mimalloc을 사용함
- 지역 MimallocArena / ArenaAllocator와 .reset()/.deinit()은 삭제함
- hot loop에서 반복 할당하던 본문이면 // PERF(port): was arena bulk-free만 남김
- allocator.dupe(u8, s) → Box::<[u8]>::from(s), 성장하면 s.to_vec()
- allocator.dupeZ → bun_str::ZStr::from_bytes(s)
- allocator.create(T) / allocator.destroy(p) → Box::new / drop
- allocator.alloc(T, n) → vec![T::default(); n].into_boxed_slice() 또는 미초기화가 필요하면 Box::new_uninit_slice(n)
- StackFallbackAllocator → heap 사용, // PERF(port): was stack-fallback
- 공통 규칙은 Zig의 OOM wrapper와 allocator 표현을 제거함
- bun.default_allocator 표현식은 삭제함
- bun.new(T, init) / bun.destroy(p) → Box::new(init) / drop(b)
- 포인터가 FFI를 *mut T로 건너가면 Box::into_raw / Box::from_raw를 사용함
- bun.handleOom(expr) → expr, Rust Vec/Box allocation은 OOM에서 abort하므로 Zig의 panic-on-OOM wrapper가 기본 동작이 됨
금지 패턴
- verify gate가 logic-bug로 표시하는 패턴은 작성하지 않음
- &'static lifetime을 맞추기 위한 Box::leak / mem::forget은 금지됨
- 필드는 &'static [T]가 아니라 Box<[T]>, Vec<T>, Cow<'static, [T]>여야 함
- Zig가 arena나 deinit으로 해제했다면 Rust도 해제해야 함
- 예외는 진짜 process-lifetime singleton뿐이며, 이 경우에도 Box::leak이 아니라 OnceLock<T> / LazyLock<T>를 사용함
- ManuallyDrop<T>는 모든 exit path에서 unsafe { ManuallyDrop::drop(..) } 또는 into_inner와 짝지어야 함
- 어디서 drop되는지 지목할 수 없으면 leak으로 간주함
- union+ManuallyDrop보다 enum을 선호함
- lifetime을 늘리기 위한 unsafe { &*(p as *const _) }는 leak과 같으므로 ownership을 고침
- borrow를 벗어나기 위한 &[u8] / Vec의 .clone()은 금지되며, capture-len-then-reslice로 재구성하거나 field type을 바꿈
- .zig에 실제 로직이 있고 상위 tier dependency가 막지 않는 non-gated, non-#[cfg(test)] 함수에서 todo!() / unimplemented!()를 쓰지 않음
동시성
- Rust는 Send/Sync auto-trait로 thread-safety를 컴파일 타임에 강제하므로, Zig의 방어적 lock이나 init-once lock 상당수는 사라짐
- Zig lock 패턴은 Rust 동시성 primitive로 치환함
- lock: Lock + has_loaded: bool + lazy init data → static X: OnceLock<T> 또는 init이 const fn에 가까우면 LazyLock<T>
- refcount 주변의 lock: Lock → Arc<T>
- single-producer→consumer queue 주변의 lock: Lock → crossbeam::channel::{bounded,unbounded} 또는 crossbeam::queue::SegQueue
- JS thread만 만지는 데이터를 보호하는 lock: Lock → lock 삭제, 타입이 JSValue/*mut JSGlobalObject를 포함해 !Sync가 되므로 공유가 컴파일되지 않음
- HTTP↔main, watcher↔main, worker-pool table처럼 실제 cross-thread mutable state는 parking_lot::Mutex<T> 또는 RwLock<T>로 유지함
- Futex/Condition wait → parking_lot::Condvar + Mutex
- std.atomic.Value(T) → core::sync::atomic::Atomic*
- ordering은 .monotonic→Relaxed, .acquire→Acquire, .release→Release, .seq_cst→SeqCst
- bun.threading.Once → std::sync::Once
- std::sync::Mutex는 쓰지 않고 항상 parking_lot을 사용함
- poisoning은 여기서 noise로 취급함
- lock을 데이터 옆에 두지 않고 Mutex<T>가 T를 소유하게 함
- Zig의 lock: Lock, table: HashMap은 Rust에서 table: Mutex<HashMap>이 됨
- lock이 방어적인지 확실하지 않으면 삭제하고 // PERF(port): was Lock-guarded — verify !Sync is sufficient를 남김
- Phase B의 cargo check가 실제 다른 thread에서 필요하면 T: Sync bound 실패로 알려주며 그때 Mutex<T>를 추가함
union(enum) crate tier 간 dispatch
- Zig의 union(enum) { A: *Foo, B: *Bar, fn run(self) { switch(self) { inline else => |p| p.run() } } }는 closed-set dynamic dispatch와 inlined arm을 결합한 형태임
- variant가 union보다 높은 tier crate에 있으면 단순 포팅은 cycle을 만들 수 있음
- cycle은 깨되 inlining을 잃지 않는 방식으로 나눔
- cold path는 low tier가 manual vtable을 정의하고 high tier가 static instance를 제공함
- per-request 호출처럼 per-tick이 아닌 대부분의 경우에 해당함
- low tier 예시는 bun_io처럼 high-tier type 이름을 알지 않는 leaf 구조임
- SourceVTable은 unsafe fn(*mut (), &[u8])인 on_read와 unsafe fn(*mut ())인 on_close를 가짐
- Source는 owner: *mut ()와 vtable: &'static SourceVTable을 가짐
- high tier 예시인 bun_runtime은 SUBPROCESS_SOURCE static을 만들고 p.cast::<Subprocess>()로 downcast해 on_read/on_close를 호출함
- heterogeneous list에서 LTO는 indirect call을 devirtualize하지 않음
- callee가 syscall이나 JS callback 같은 실제 일을 하는 경우에는 acceptable하며 // PERF(port): was inline switch를 남김
- hot path는 low tier가 (tag: u8, ptr: *mut ())를 저장하고 iterator를 노출하며, high tier가 match loop를 소유함
- per-tick dispatch에 해당함
- low tier bun_event_loop 예시는 #[repr(transparent)] pub struct TaskTag(pub u8);, pub struct Task { pub tag: TaskTag, pub ptr: *mut () }, Queue::drain() iterator임
- high tier bun_runtime의 run_tasks는 q.drain()을 순회하며 match tag.0로 PromiseTask, TimerTask 등 variant별 direct call을 수행함
- unknown arm은 unsafe { core::hint::unreachable_unchecked() }로 처리함
- direct call per arm이므로 LLVM이 Zig처럼 inline함
- hoisted-match를 쓰는 hot-path 목록은 제한됨
- bun_event_loop::Task / ConcurrentTask microtask queue
- bun_aio::FilePoll::Owner의 TaggedPointerUnion, 약 13개 variant
- bun_event_loop::EventLoopTimer::Tag
- bun_io::Source
- bun_threading::WorkPool::Task
- hot path에는 Box<dyn Trait> / enum_dispatch를 사용하지 않음
- Zig가 이미 *anyopaque + fn-ptr로 indirect dispatch를 쓴 곳에서만 dyn Trait을 사용함
- debug/crash hook은 low tier가 static HOOK: AtomicPtr<()>를 정의하고 high tier가 init 때 fn pointer를 씀
- crash_handler dump callback과 safety allocator check가 해당함
- one-shot registration이며 vtable을 쓰지 않음
포인터와 소유권
- Bun pointer abstraction은 Rust ownership type으로 직접 옮김
- bun.ptr.Owned(T) → Box<T>
- bun.ptr.Shared(*T) → Rc<T>, 항상 single-thread이고 non-intrusive로 다룸
- weak-count word를 아끼기 위해 custom bun_ptr::Shared<T>를 도입하지 않음
- tree-wide 4회 사용에서 allocation당 8 bytes는 무시 가능하며, custom type은 Rc::downgrade/make_mut/get_mut를 잃음
- hot array가 의심되면 // PERF(port): Rc weak-count header — profile in Phase B를 남김
- bun.ptr.AtomicShared(*T) → Arc<T>, 항상 atomic
- refcount는 기본적으로 Rc<T> / Arc<T>를 사용함
- bun.ptr.RefCount(...) / ThreadSafeRefCount(...) → 기본은 Rc<T> / Arc<T>
- raw pointer가 FFI를 건너고 C++가 ref()/deref()를 호출하는 경우에만 intrusive bun_ptr::RefCounted trait을 사용함
- 해당 경우는 .classes.ts payload, extern fn 노출, container_of!로 회수되는 값임
- 88개 사용 중 약 14개만 intrusive가 필요함
- 확실하지 않으면 Rc를 쓰고 verify gate / cargo-check가 FFI break를 알려주게 함
- tagged pointer는 용도에 따라 다르게 처리함
- dispatch 문맥의 bun.ptr.TaggedPointer / TaggedPointerUnion은 쓰지 않고 dispatch 규칙에 따라 (tag: u8, ptr: *mut ()) 두 필드로 저장함
- 8→16 byte 비용은 수십 개 instance 수준에서는 무시 가능함
- cache-line 이유로 8-byte packing이 정말 필요하면 // PERF(port): was TaggedPointer pack을 남김
- 별도 collection mapping에서는 bun.ptr.TaggedPointer → bun_collections::TaggedPtr, #[repr(transparent)] u64, addr:49 + tag:15
- bun.ptr.TaggedPointerUnion(Types...) → 항상 bun_collections::TaggedPtrUnion<(T1, T2, ...)>
- packed u64 layout은 array 저장과 hash에서 중요하므로 Rust enum으로 확장하지 않음
- Rust enum은 8 bytes가 아니라 16 bytes가 됨
- 기타 포인터/컬렉션 ownership mapping이 정해짐
- bun.ptr.Cow(T) → Cow<'_, T> 또는 Arc<T> + Arc::make_mut
- bun.ptr.WeakPtr(T, field)는 intrusive deprecated 구조이므로 embedded WeakPtrData 위에 *mut T + manual ref/deref를 유지하거나 owner를 Rc<T>로 이관해 std::rc::Weak를 사용함
- owner가 intrusive인데 std::rc::Weak / std::sync::Weak로 맹목 치환하지 않음
- bun.HiveArray(T, N) → bun_collections::HiveArray<T, N>
- 별도 deinit()이 있는 *T field → unique owner이면 Box<T>, shared이면 *mut T + // SAFETY:
- intrusive list와 @fieldParentPtr 패턴은 유지함
- raw pointer와 core::mem::offset_of!를 사용함
- Phase A에서 Pin<Box<T>>로 바꾸지 않음
컬렉션
- std.ArrayList(T) / std.ArrayListUnmanaged(T)는 crate 성격에 따라 나뉨
- non-AST crate에서는 Vec<T>를 쓰고 allocator arg를 모두 제거함
- AST crate에서는 Zig가 arena를 공급했다면 bumpalo::collections::Vec<'bump, T>를 쓰고, 아니면 Vec<T>를 씀
- managed/unmanaged 구분은 사라짐
- ArrayList method mapping은 Vec 또는 bumpalo Vec에 공통 적용됨
- .append(x) → .push(x)
- .appendSlice(s) → .extend_from_slice(s)
- .appendAssumeCapacity(x) → .push(x) + // PERF(port): was assume_capacity
- .ensureTotalCapacity(n) → .reserve(n.saturating_sub(v.len()))
- .ensureTotalCapacityPrecise(n) → .reserve_exact(..)
- .toOwnedSlice() → .into_boxed_slice() 또는 .into_bump_slice()
- .items → .as_slice() / &v
- .clearRetainingCapacity() → .clear()
- .swapRemove(i) → .swap_remove(i)
- HashMap 계열은 Bun collection을 사용해 hashing과 iteration 차이를 유지함
- std.AutoHashMap(K,V) → bun_collections::HashMap<K,V>, wyhash 사용
- std.StringHashMap(V) → bun_collections::StringHashMap<V>
- std.AutoArrayHashMap(K,V) / std.StringArrayHashMap(V) → bun_collections::ArrayHashMap<K,V>, wyhash와 insertion-order iteration을 유지하며 .values()는 contiguous slice를 반환함
- HashMap이나 indexmap으로 대체하지 않음
- std::collections::HashMap은 SipHash와 다른 iteration order 때문에 behavioral diff가 생기므로 쓰지 않음
- Bun 전용 collection은 대응 crate type으로 유지함
- bun.MultiArrayList(T) → bun_collections::MultiArrayList<T>, SoA
- bun.BabyList(T) → bun_collections::BabyList<T>, ptr+len+cap, #[repr(C)]
- std.BoundedArray(T,N) → bun_collections::BoundedArray<T, N>
- bun.StringMap → bun_collections::StringMap
- bun.HiveArray(T, N) → bun_collections::HiveArray<T, N>
- enum 기반 collection은 dense/sparse 성격을 보존함
- std.EnumArray(E, V) → enum_map::EnumMap<E, V>와 E의 #[derive(enum_map::Enum)]
- variant로 indexed되는 dense [V; N]이며 derive의 associated Array<V> type이 count를 숨김
- stable Rust에서는 generic하게 [V; <E as Enum>::COUNT]를 쓸 수 없음
- std.EnumArray를 HashMap으로 바꾸지 않음
- std.EnumSet(E) → enumset::EnumSet<E>와 #[derive(enumset::EnumSetType)]
- storage는 variant count에 맞는 가장 작은 uN임
- bitflags!는 power-of-two 값을 손으로 할당하고 새 type을 정의해야 하며 기존 #[repr(uN)] enum을 감쌀 수 없으므로 쓰지 않음
- sparse std.EnumMap(E, V) → enum_map::EnumMap<E, Option<V>>
- discriminant overhead가 중요하면 { present: enumset::EnumSet<E>, values: [MaybeUninit<V>; N] }를 직접 구현하고 // PERF(port)를 남김
- compile-time map과 bitset도 전용 대응을 사용함
- bun.ComptimeStringMap(V, .{...}) → static MAP: phf::Map<&'static [u8], V> = phf::phf_map! { b"key" => val, ... };
- bun.ComptimeEnumMap(E) → E의 @tagName으로 만든 phf::Map<&'static [u8], E>
- bun.bit_set.IntegerBitSet(N) → bun_collections::IntegerBitSet<N>, #[repr(transparent)] uN, inline, heap 없음
- bun.bit_set.StaticBitSet(N) / ArrayBitSet(usize, N) → bun_collections::StaticBitSet<N>, [usize; (N+63)/64], inline, heap 없음
- bun.bit_set.DynamicBitSet / DynamicBitSetUnmanaged → bun_collections::DynamicBitSet, heap-backed Box<[usize]>
- bun.bit_set.AutoBitSet → bun_collections::AutoBitSet, Bun-specific runtime static-or-dynamic이며 std/crate 등가물이 없음
JSC 타입
- bun_jsc::JSValue는 #[repr(transparent)], Copy, Clone, Eq, PartialEq인 pub struct JSValue(i64, PhantomData<*const ()>);로 표현함
- PhantomData<*const ()>는 !Send + !Sync를 만들기 위한 것임
- negative impl인 impl !Send는 nightly-only이며 feature(negative_impls), tracking #68318이 필요함
- lifetime은 없음
- 값은 conservative stack scan으로만 살아 있으며 stack/registers만 대상임
- heap-allocated Rust struct에 bare JSValue를 field로 저장하지 않음
- conservative scan은 stack/registers만 커버함
- struct field에는 bun_jsc::Strong root, bun_jsc::JsRef self-wrapper ref, 또는 codegen된 own: property인 C++ side WriteBarrier를 사용함
- Box/Arc/Vec payload 안의 JSValue field는 use-after-free가 됨
- JSC 기본 포인터와 상수 mapping이 정해짐
- globalObject: *JSGlobalObject → global: &JSGlobalObject
- callframe: *CallFrame → frame: &CallFrame
- .js_undefined → JSValue::UNDEFINED
- .jsNull() / .null → ::NULL
- .jsBoolean(b) → JSValue::from(b)
- .true / .false → ::TRUE / ::FALSE
- .zero → JSValue::ZERO, encoded 0
- ZERO는 UNDEFINED와 다르며 “no value / exception pending”을 뜻하고, host fn이 throw 후 반환해야 하는 값임
- value == .zero 검사는 value.is_empty()가 됨
- ensureStillAlive는 GC 관련 point-in-time 보호로 옮김
- value.ensureStillAlive() → value.ensure_still_alive()
- 구현은 if value.is_cell() { core::hint::black_box(value.0); }
- Zig의 doNotOptimizeAway와 일치하며 non-cell에는 no-op임
- black_box는 Rust 1.66부터 stable임
- typed-array .as_slice()나 string .characters8()처럼 value에서 얻은 interior pointer의 마지막 사용 이후 호출해야 하며 이전에 호출하지 않음
- scope-long 보호가 필요하면 let _keep = EnsureStillAlive(value);를 쓰고 Drop에서 black_box를 호출함
- release-only GC crash가 계속되면 JSC와 맞춘 inline asm unsafe { core::arch::asm!("", in(reg) value.0, options(nostack, preserves_flags)); }로 올림
- black_box는 std 문서상 best-effort이고 JSC가 쓰는 "memory" clobber가 없음
- call argument용 JSValue slice에는 Vec<JSValue>를 쓰지 않음
- Vec backing storage는 Rust heap에 있고 stack-scanned가 아님
- bun_jsc::MarkedArgumentBuffer를 사용해 VM에 root로 등록하거나 fixed-size on-stack [JSValue; N]을 사용함
- loop 중 to_js()/get_index()로 element를 만들면 earlier element가 loop 도중 collect될 수 있음
- JSRef와 Strong은 서로 다른 root/참조 semantics를 가짐
- JSRef field → non-generic bun_jsc::JsRef
- JsRef는 Weak(JSValue) | Strong(Strong.Optional) | Finalized tagged union임
- .weak arm은 JSC::Weak가 아니라 bare JSValue이며, codegen된 finalize()가 .finalized로 바꾸기 때문에만 sound함
- finalize: true가 없는 struct에는 JsRef를 쓰지 않음
- Strong / Strong.Optional → bun_jsc::Strong
- Strong은 vm.heap.handleSet()에서 할당된 HandleSlot이며 JSC::Strong<T>와 같은 root set을 쓰는 GC root이고 Drop에서 slot을 해제함
- Rust struct 자체가 JS wrapper인 m_ctx에 의해 소유된다면 wrapper나 wrapper에 도달 가능한 대상을 가리키는 Strong은 영구 leak이므로 JsRef를 사용함
- bun_jsc::Strong과 bun_jsc::JsRef는 PhantomData<*const ()>로 !Send + !Sync임
- HandleSlot은 VM의 HandleSet 소유이고 Drop은 JS thread에서 실행되어야 함
- 이를 Arc<T>에 넣고 thread-pool thread에서 drop하면 UB임
- extra memory reporting은 alloc-side와 visit-side를 모두 맞춰야 함
- globalThis.vm().reportExtraMemory(n) → global.vm().deprecated_report_extra_memory(n)
- cell이 없는 경로이며 Zig binding과 정확히 맞음
- 이 호출은 buffer append나 slice clone 같은 incremental-growth path임
- non-deprecated heap.reportExtraMemoryAllocated(cell, n)는 .classes.ts의 estimatedSize: true가 있을 때 codegen이 construction에서 호출하므로 hand-port하지 않음
- Zig type이 pub fn estimatedSize(...) usize를 구현하면 유지함
- codegen은 construct/__create의 reportExtraMemoryAllocated와 visitChildren의 reportExtraMemoryVisited를 모두 연결함
- construction 이후 후속 growth에 대해서만 deprecated_report_extra_memory(delta)를 수동 호출함
- alloc-side만 있고 visit-side가 없으면 back-to-back full GC가 생기고, visit-side만 있고 alloc-side가 없으면 OOM이 됨
- host function은 attribute macro가 JSC ABI shim을 생성하게 함
- fn(*JSGlobalObject, *CallFrame) bun.JSError!JSValue 또는 JSHostFnZig → #[bun_jsc::host_fn] pub fn name(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue>
- callconv(jsc.conv) raw form인 JSHostFn은 attribute macro가 생성하므로 직접 작성하지 않음
- .classes.ts type의 method host fn은 #[bun_jsc::host_fn(method)] pub fn name(this: &mut Self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue>
- getter는 #[bun_jsc::host_fn(getter)] pub fn get_foo(this: &Self, global: &JSGlobalObject) -> JsResult<JSValue>
- setter는 #[bun_jsc::host_fn(setter)] pub fn set_foo(this: &mut Self, global: &JSGlobalObject, value: JSValue) -> JsResult<bool>
- macro는 m_ctx를 *mut Self로 downcast하는 callconv(jsc.conv) shim을 생성함
- bun.JSError!T → bun_jsc::JsResult<T>, Result<T, JsError> alias이며 JsError는 Thrown, OutOfMemory, Terminated를 가짐
- .classes.ts 기반 type은 C++ JSCell wrapper가 계속 생성 C++에 남음
- Rust struct는 m_ctx payload임
- #[bun_jsc::JsClass]를 derive하면 codegen이 toJS/fromJS/hasPendingActivity를 연결함
- visitChildren를 직접 작성하지 않음, WriteBarrier field는 C++ side에 있음
- hasPendingActivity()는 GC thread에서 mutator와 동시에 실행됨
- JSC calling convention을 사용해야 하며 형태는 #[bun_jsc::host_call] extern fn(*mut Self) -> bool
- ABI rewrite는 host_fn과 같아서 Windows-x64에서는 "sysv64", 그 외에는 "C"임
- hasPendingActivity()는 Ordering::Acquire로 Atomic* field만 읽고, allocate, lock, JS 접근을 하지 않음
- 단일 busy/idle edge가 있으면 hasPendingActivity보다 JsRef upgrade/downgrade를 선호함
- .classes.ts의 finalize: true는 Rust struct에 pub fn finalize(this: *mut Self)를 구현함
- finalize는 lazy sweep 중 mutator thread에서 실행되며 다른 cell이 이미 swept일 수 있으므로 JSValue/Strong content를 만지지 않음
- self.this_value.finalize()를 먼저 호출한 뒤 native resource를 drop함
- prompt cleanup에 의존하지 않고 explicit close()를 노출함
FFI와 플랫폼 조건부
- Zig의 extern fn us_socket_write(s: *Socket, data: [*]const u8, len: c_int) c_int;는 Rust에서 unsafe extern "C" block으로 옮김
- 예시는 pub fn us_socket_write(s: *mut Socket, data: *const u8, len: c_int) -> c_int;
- item은 기본적으로 unsafe fn이며, caller가 safe로 취급해도 되는 함수는 Rust 1.82+에서 safe fn으로 작성함
- 모든 extern fn block은 해당 영역의 *_sys crate로 옮김
- 파일에 extern이 있지만 아직 *_sys가 아니라면 그대로 두고 // TODO(port): move to <area>_sys를 남김
- callconv(.c) → extern "C"
- JSC host fn은 JSC 규칙처럼 #[bun_jsc::host_fn]으로 작성함
- user-facing fn에는 extern을 붙이지 않음
- attribute macro가 Windows-x64에서는 "sysv64", 그 외에는 "C" ABI를 생성함
- Rust는 ABI 위치에 macro를 허용하지 않으므로 extern jsc_conv!()를 작성할 수 없음
- exported fn은 #[unsafe(no_mangle)] pub extern "C" fn name(...)로 옮김
- @export와 comptime { @export(...) } 모두 해당함
- edition 2021에서는 plain #[no_mangle]도 작동하지만, unsafe extern 스타일에 맞춤
- if (Environment.isWindows) { ... } else { ... }는 #[cfg(windows)] { ... }와 #[cfg(not(windows))] { ... }로 옮김
- 단순한 value-level 선택에는 if cfg!(windows) { ... }를 사용할 수 있음
- if cfg!(windows)는 두 branch를 type-checker와 monomorphization에 남기므로, disabled branch가 platform-only item을 참조하면 #[cfg(...)] 형태를 사용함
- 기타 environment mapping도 Rust cfg로 옮김
- Environment.isDebug → cfg!(debug_assertions)
- Environment.isPosix → #[cfg(unix)]
- Environment.os == .windows/.mac/.linux/.wasm → #[cfg(target_os = "windows"/"macos"/"linux")]
- Windows arm에는 #[cfg(windows)]도 가능하며 isWindows와 동일하게 취급함
번역하지 않을 것과 출력 형식
- 파일 하단의 @import line은 1:1로 옮기지 않고 상단의 use bun_<area>::...;로 처리함
- pub const X = @import("../foo_jsc/..").y; alias line은 삭제함
- Rust에서는 to_js/from_js가 *_jsc crate에 있는 extension-trait method이고, base type은 jsc를 언급하지 않음
- comptime { _ = @import(...); } force-reference block은 제거함
- generated file은 3-line .rs stub으로 대체함
- 대상은 *_generated.zig, grapheme_tables.zig, boringssl_sys/boringssl.zig, libuv_sys/libuv.zig, schema.zig
- stub에는 // GENERATED: re-run <generator> with .rs output를 둠
- Zig test block은 Rust test module로 옮김
- test "..." { ... } → #[cfg(test)] mod tests { #[test] fn ...() { ... } }
- .rs 파일 끝에는 port status trailer comment를 붙임
// ──────────────────────────────────────────────────────────────────────────
// PORT STATUS
// source: src/<area>/<file>.zig (NNN lines)
// confidence: high | medium | low
// todos: N
// notes: <one line: anything Phase B needs to know>
// ──────────────────────────────────────────────────────────────────────────
- confidence: low는 “logic is probably wrong, re-read the Zig in Phase B”를 뜻함
- confidence: medium은 “types/imports will need fixing but logic is right”를 뜻함
- confidence: high는 “should compile with only mechanical import fixes”를 뜻함