Bun(JS 런타임)이 Zig에서 Rust로 바이브 포팅되고 있음

1 week ago 12
  • 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로 매핑함
    • Box, Rc, Arc를 사용함
  • 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은 제거함
    • Rust는 pub인 항목을 link함
  • 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”를 뜻함
Read Entire Article