9月19日、bun.shで「Compile and run C in JavaScript」と題した記事が公開された。この記事では、JavaScriptからCを直接コンパイルし実行できる機能について詳しく紹介されている。この新機能は、システムプログラミングにおけるCやC ABI(Application Binary Interface)が果たしてきた重要な役割に焦点を当て、Bun v1.1.28で導入された実験的サポートを説明している。
以下に、その内容を詳しく解説する。
CとJavaScriptの統合
C言語はシステムプログラミングにおける過去、現在、未来を代表する言語である。圧縮、暗号化、ネットワーキングなど、あらゆるコンピュータシステムの基盤を支えている。さらに、C++やRust、Zigといった言語もC ABIと互換性があり、Cライブラリとして提供されていることが多い。つまり、Cはシステムプログラミングにおける共通言語であり、これをJavaScriptとシームレスに結びつけることが求められていた。
Bun v1.1.28で導入された「bun:ffi」は、JavaScriptからCコードを直接コンパイルし実行する実験的機能を提供する。これにより、従来のN-APIやWASMを使った方法に比べ、パフォーマンスが大幅に向上する可能性がある。
例: hello.cのコンパイルと実行
以下のCコード「hello.c」は、Bunを使ってJavaScriptから直接コンパイルされ、実行される。
#include <stdio.h>
void hello() {
printf("You can now compile & run C in Bun!\n");
}
このCコードをJavaScriptで呼び出すには、次のように記述する。
import { cc } from "bun:ffi";
export const {
symbols: { hello },
} = cc({
source: "./hello.c",
symbols: {
hello: {
returns: "void",
args: [],
},
},
});
hello();
この例では、BunのForeign Function Interface(FFI)を使って、Cコードをコンパイルし、JavaScriptからhello
関数を呼び出すことができる。この方法は、システムライブラリを使用する際に非常に効率的であり、従来のN-APIやWASMを使用する方法と比較して、いくつかの利点がある。
N-APIの限界と問題点
これまでJavaScriptでシステムライブラリを使用するには、N-APIを介したアプローチが一般的であった。N-APIは、JavaScriptとネイティブコードを接続するためのランタイム非依存のC APIであるが、いくつかの問題が存在する。
CI環境でのビルドの問題
N-APIを使ったネイティブアドオンは、node-gyp
を用いたコンパイルが必要であり、これにはPython 3やC++コンパイラが必要となる。フロントエンドのJavaScriptアプリケーションをビルドする際に、このような追加の依存関係を導入することは、しばしば開発者にとって予期せぬ負担となる。複雑なビルドプロセス
ライブラリのメンテナーにとって、複数のプラットフォームに対応したビルドを維持するのは非常に複雑である。例えば、@napi-rs/canvas
のpackage.json
には、異なるプラットフォームごとの依存関係が定義されており、そのメンテナンスが大きな負担となる。
"optionalDependencies": {
"@napi-rs/canvas-win32-x64-msvc": "0.1.55",
"@napi-rs/canvas-darwin-x64": "0.1.55",
"@napi-rs/canvas-linux-x64-gnu": "0.1.55"
}
- パフォーマンスの問題
JavaScriptからN-API経由でネイティブコードを呼び出す際には、約3倍のオーバーヘッドが発生する。N-APIはランタイム非依存であるため、引数の型チェックやメモリアロケーションに多くの処理が必要となり、結果としてパフォーマンスが低下する。
WebAssembly(WASM)の制約
WASMはN-APIのビルドプロセスやパフォーマンスの問題を回避するための手段として注目されているが、システムライブラリの利用には制約がある。特に、WASMのメモリモデルが分離されているため、システムAPIにアクセスできない問題がある。
システムコールの制限
例えば、macOSのKeychain APIを使用してパスワードを安全に保存する場合、WASMはそのようなシステムAPIにアクセスできない。このような状況では、WASMの採用が難しくなる。
BunによるネイティブCのコンパイルと実行
Bunは、これらの制約を解消するため、JavaScriptからネイティブCを直接コンパイルし、実行できる機能を提供している。このアプローチにより、N-APIやWASMの複雑さを回避し、パフォーマンスを向上させることが可能である。
例: ランダム数生成
次のCコードは、JavaScriptから直接コンパイルされ、実行されるランダム数生成関数である。
#include <stdio.h>
#include <stdlib.h>
int myRandom() {
return rand() + 42;
}
JavaScript側では、以下のコードでCの関数を呼び出している。
import { cc } from "bun:ffi";
export const {
symbols: { myRandom },
} = cc({
source: "./myRandom.c",
symbols: {
myRandom: {
returns: "int",
args: [],
},
},
});
console.log("myRandom() =", myRandom());
この方法により、CコードをJavaScript内でコンパイルし、ネイティブのシステムライブラリを効率的に利用することができる。
高速なコンパイルと低オーバーヘッド
BunはTinyCCを使用することで、Cコードのコンパイル速度を大幅に向上させている。例えば、上記のmyRandom.c
をコンパイルする時間はわずか5.16msであり、非常に高速である。また、bun:ffi
は、ネイティブコード呼び出しのオーバーヘッドを最小限に抑え、従来のN-APIに比べて圧倒的に低いオーバーヘッドを実現している。
詳細はCompile and run C in JavaScriptを参照していただきたい。