ESPNThe only MLB playoff preview you need: World Series odds, likely MVPs and how far all 12 teams will goThe Jerusalem PostHezbollah's Nasrallah commemoration exposes its lost standing in Lebanon, expert saysPunchAI should complement, not replace, judicial officers – A’Ibom CJESPN DeportesAl Rojas Vivo: Álvarez, Sánchez, Durán, Stewart y Espada, los latinos de 2026Inquirer‘Most unique’ birds documented in Apayao biosphereColliderNew James Bond Release Officially Adds a 'Game of Thrones' StarAnime News NetworkStalled Despera Anime Project Gets MangaPopular ScienceBald eagles Shadow and Kaydee share a peaceful morning, Jackie’s memorial service approachesVariety‘Stillwater’: Amazon Series Based on Graphic Novel Casts Ben Hardy in Lead RoleBBC عربي"محادثات مرتقبة غير مباشرة" بين واشنطن وطهران في نيويورك، وخامئني يقول إن "القوات الأمريكية ستُجبر على مغادرة بحر العرب"20 Minuten«Bin 100 Meter reingelaufen»: Bodensee-Pegel so tief wie noch nieBBC NewsBest thing we can offer young people is a job, not benefits, says chancellor
The Daily Newsstand · Free, Always
Monday, September 28, 2026

[Перевод] Почему я пишу Bun‑native альтернативу napi‑rs

Translate

Я работаю над bffi‑rs, экспериментальным фреймворком для нативных биндингов Bun, написанным на Rust.

Проект появился из довольно практичной задачи. Я хотел сделать небольшое desktop‑приложение с нативным WebView, но при этом оставить Bun основным рантаймом.

В экосистеме уже есть решения для подобных приложений. Например, некоторые интеграции WebView для Bun используют Rust и napi-rs. В варианте Electrobun, который я рассматривал, для WebView используется Wry, а связка между Rust и JavaScript строится через napi-rs. Эти проекты решают реальные задачи, но у меня появился другой вопрос: что будет, если строить нативную часть приложения вокруг Bun, а не вокруг совместимости с Node.js?

Откуда взялась идея

В Bun 1.4 появилось много изменений, важных именно для нативных интеграций. bun:ffi стал частью JavaScriptCore, горячие FFI‑вызовы получили возможность компилироваться JIT в прямые вызовы C‑функций, а новый тип аргумента buffer_length позволил передавать указатель и длину буфера так, чтобы они не могли случайно расходиться.

Bun заявляет ускорение до 3 раз для ряда FFI‑сценариев в версии 1.4. В релизах 1.4.x также были изменения и исправления, связанные с Worker‑потоками, доставкой событий между потоками, JavaScriptCore, GC и передачей указателей через FFI.

Это заставило меня по‑другому посмотреть на границу между JavaScript и Rust. Если приложение работает на Bun, а нативный модуль подключается через Node‑API, то между приложением и Rust появляется интерфейс совместимости, изначально ориентированный прежде всего на Node.js.

Это может быть правильным компромиссом, если главная цель проекта заключается в переносимости. Но такой слой не проектировался вокруг собственного FFI и модели потоков Bun.

napi-rs сам описывает себя как фреймворк для создания Node.js add‑on‑модулей через Node‑API. В его issue tracker есть отдельные обсуждения о том, что Bun не является строго поддерживаемым рантаймом, в том числе случаи, когда асинхронное поведение Rust‑кода под Bun отличается от Node.js.

В моём случае это тоже оказалось не только теорией. Когда я экспериментировал с WebView, часть кода, которая выглядела нормально с точки зрения Node‑API, под Bun могла завершаться раньше времени или падать.

Поэтому я решил попробовать другой путь: не адаптировать Node‑API‑библиотеку под Bun, а построить нативный слой вокруг экспериментального bun:ffi и поведения, которое появилось в Bun 1.4.x.

Цель проекта не в том, чтобы объявить bun:ffi более зрелым, чем Node‑API. Это было бы неправдой. И bun:ffi, и bffi являются экспериментальными библиотеками. Я хочу сделать Bun‑native слой, который напрямую использует возможности Bun, не скрывает границу между рантаймом и Rust и позволяет принимать отдельные решения для callbacks, workers, буферов и event loop.

Я также сознательно оставил MIT‑лицензию. Проект можно форкнуть, изменить ABI, переделать упаковку нативных бинарников или адаптировать реализацию под другой сценарий. Мне интереснее увидеть, как этот дизайн будут критиковать и менять, чем превратить его в закрытую чёрную коробку.

Сейчас главная проблема проекта уже не только в реализации. Мне не хватает обратной связи от людей, которые могли бы использовать такой инструмент. Недавно я увидел около 600 активных скачиваний npm‑пакета, но почти не получил обсуждений или отзывов. Поэтому я не понимаю, решает ли текущий дизайн реальную проблему или интересен только мне.

Дальше я расскажу, что именно уже сделано и какие решения всё ещё можно спокойно пересмотреть.

Технические детали, которые повлияли на направление проекта, описаны в релизе Bun 1.4, Bun 1.4.1 и Bun 1.4.2. Сам napi-rs описывает проект как Node‑API‑фреймворк для Node.js в своём репозитории.

Почему не napi‑rs

napi-rs построен вокруг Node‑API. Это хороший выбор, если нужно выпускать один нативный модуль для нескольких Node‑совместимых рантаймов.

Моя задача другая. Я хочу использовать собственный bun:ffi и строить binding‑модель вокруг его реальных ограничений, а не прятать их за compatibility layer.

Это означает, что проект сознательно принимает меньшую область совместимости ради более явной архитектуры:

  • нативный код компилируется в Rust cdylib и загружается через bun:ffi;

  • на границе используется тонкий C ABI, а не Node‑API;

  • C‑функция возвращает статус ошибки;

  • фактический результат передаётся через последний out‑параметр;

  • ошибки передаются через thread‑local error slot;

  • полный экспортируемый API описывается сгенерированными дескрипторами;

  • TypeScript‑обёртки создаются на основе этих дескрипторов.

bffi-rs не совместим по API с napi-rs и не пытается им быть. Если нужна поддержка Node.js или Deno, это неподходящий инструмент. Если нужен Bun‑native binding layer, который явно показывает, что происходит на границе, именно эту задачу я пытаюсь решить.

Один источник правды для нативного API

Основной пользовательский macro выглядит так:

#[bffi]
pub fn add(a: u32, b: u32) -> u32 {    a.wrapping_add(b)
}

Он создаёт три артефакта:

  1. исходную Rust‑функцию;

  2. extern "C" shim с ABI bffi;

  3. compile‑time descriptor с именем функции, параметрами, типом результата, документацией и ABI‑сигнатурой.

Дескрипторы объединяются в ModuleDef. Бинарник emit-json записывает его в .bffi/bffi.api.json. Пакет @z2net/bffi проверяет схему, генерирует .bffi/api.gen.ts, находит нативный бинарник и открывает его через bun:ffi.

Обычная точка входа выглядит так:

import { bffi } from "@z2net/bffi";
import type { Api } from "./.bffi/api.gen.ts";
const api: Api = await bffi();
api.add(1, 2);

Для каждой функции не нужно вручную писать отдельную binding‑обёртку. API генерируется под конкретный модуль, а схема встраивается в сгенерированный файл, чтобы TypeScript проверял точные типы, которые использует приложение.

Перед первым вызовом loader также выполняет ABI‑ и exports‑hash handshake. Если manifest устарел или не соответствует бинарнику, ошибка возникает заранее, а не превращается в непонятный вызов отсутствующего символа.

Граница должна быть скучной

Именно на FFI‑границе заканчиваются гарантии Rust. Поэтому я хотел сделать ownership и обработку ошибок явными, а не разносить эти правила по каждой отдельной binding‑функции.

Основные правила сейчас такие:

  • данные по умолчанию копируются при переходе через границу;

  • zero‑copy доступен только через явно названный путь bffi::unsafe_zero_copy;

  • объекты, буферы и callbacks используют generational u64 handles с type tags;

  • устаревший handle не может попасть в повторно использованный слот;

  • строки используют UTF-8 как единую каноническую кодировку;

  • 64-битные числа остаются точными JavaScript bigint;

  • в release‑сборках Rust panic перехватывается на ABI‑границе и превращается в JavaScript Error;

  • публичный Rust API остаётся safe, а внутренний unsafe спрятан внутри модулей фреймворка.

C ABI не может напрямую вернуть Rust Result. Ошибка возвращается числовым кодом, а подробный BffiError забирается из last‑error slot. Типизированные domain errors могут использовать собственные стабильные коды в диапазоне 0x1000..=0xFFFF. На стороне JavaScript вариант ошибки доступен через e.name, а его поля через e.payload.

Например, Rust‑ошибка может перейти в JavaScript без потери структуры:

#[derive(BffiError, Debug)]
pub enum UsersError {    #[bffi(code = 0x1001)]    NotFound { id: u64 },
}
try {    api.find_user(99n);
} catch (error) {    error.code;    // 0x1001    error.name;    // "NotFound"    error.payload; // [99n]
}

Асинхронный код не выполняется тайно в JavaScript‑потоке

#[bffi_async] превращает Rust future в типизированный Promise<T>. Worker‑потоки опрашивают future, но не вызывают JavaScript напрямую. Когда future завершается, bffi кодирует результат, ставит задачу в очередь и доставляет её в JavaScript‑потоке.

Доставка выполняется через явный pump:

await pumpUntil(api.compute(5), () => api.loopPump());

Нативный event loop Bun нельзя просто перехватить из Rust cdylib. Поэтому bffi не устанавливает скрытый timer и не делает вид, что Promise сам собой разрешится. Код, в который встроен модуль, сам решает, когда и как вызывать pump очереди bffi.

Та же модель используется для callbacks из native‑потоков. invoke_wait отправляет callback в JavaScript‑поток, ждёт ответ с обязательным timeout и возвращает результат native‑коду. Это нужно для GUI и event‑driven библиотек, где обработчик работает в OS‑потоке, но должен синхронно обратиться к JavaScript.

В репозитории есть рабочий пример с Wry: native WebView‑поток, IPC‑сообщения, JavaScript callbacks и ответ, который возвращается обратно в страницу. Именно поэтому bffi не ограничивается простым вызовом нативной функции.

В проект входит и упаковка нативных модулей

Runtime является только частью задачи. В проекте также есть @z2net/bffi-cli. Он умеет создать каркас binding‑проекта, собрать Rust crate, проверить сгенерированные файлы, сгенерировать TypeScript API, упаковать native binary и скачать готовый release artifact.

Нативные модули распространяются через platform packages по модели, похожей на napi-rs: основной TypeScript‑пакет фиксирует platform packages в optionalDependencies, а каждый platform package содержит нужный .dll, .so или .dylib.

Reference package рассчитан на Windows, Linux glibc, Linux musl и macOS. Версии platform packages фиксируются точно, потому что сочетание нового JavaScript loader со старым native binary легко превращается в сломанный release.

Что уже проверяется примерами

В отдельном репозитории bffi‑examples находятся end‑to‑end модули для следующих сценариев:

  • SQLite handles и реальные запросы к базе;

  • Rust futures как JavaScript Promises;

  • cancellation и timeouts;

  • event‑loop pumping и ошибки wrong‑thread;

  • callbacks в обе стороны;

  • несколько Bun Worker isolates;

  • records, enums и Vec<T> sequences;

  • pull‑ и push‑streams как async iterators;

  • typed domain errors;

  • Wry WebView с IPC из native‑потока.

Это не просто набор фрагментов кода. Каждый пример проходит полный путь: Rust source, macro expansion, cdylib, loader JSON, generated TypeScript, dlopen и Bun‑тесты.

Есть и небольшой benchmark для reference native module. Специализированная сгенерированная обёртка показала примерно 172 миллиона вызовов в секунду на benchmark runner GitHub Actions, а generic wrapper на том же модуле показал примерно 11 миллионов. Это не универсальный benchmark для любого компьютера, а проверка того, зачем default‑путь использует специализированную генерацию.

Честный статус проекта

И bun:ffi, и bffi являются экспериментальными библиотеками. Bun прямо указывает, что у bun:ffi есть известные ошибки и ограничения. bffi тоже экспериментален, поэтому я пока не рекомендую использовать его как production dependency.

Я не планирую бросать проект. Наоборот, хочу продолжать его развивать, пока архитектуру ещё можно менять без необходимости ломать большую установленную базу.

Сейчас мне особенно нужна обратная связь от людей, которые работают с Bun, Rust, native modules или FFI.

  • Нужен ли вам Bun‑only binding framework, или переносимость важнее?

  • Понятна ли модель с явным pump для async‑кода и callbacks?

  • Разумна ли политика copy‑by‑default?

  • Какие части ABI или generated API вы бы перепроектировали?

  • Оправдывает ли GUI и event‑driven сценарий дополнительную сложность?

  • Что помешало бы вам попробовать проект в реальном приложении?

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.