[Bun] JavaScriptでC言語のファイルを簡単に実行する
Introduction
Bunは高速なJavaScriptランタイムです。
ここにあるように、Bunは(v1.1.28〜)jsからCのコードをコンパイルして実行するためのexperimentalサポートを導入しました。
この「bun:ffi」ですが、JavaScriptからCコードを直接コンパイルして実行します。
これはN-API(Node-API)やWASMを使った既存の手法と比較した場合、
N-APIよりシンプルであったりWASMの制約を気になくてよかったりなど、メリットがあります。
ここではBunのffi(TinyCC)をつかってCのコードを実行してみます。
Environment
- MacBook Pro (14-inch, M3, 2023)
- OS : MacOS 14.5
- gcc : Apple clang version 15.0.0
- node : v22.5.1
- Deno : 1.46.3
- Bun : 1.1.29
Setup
Bunのセットアップは↓のように実行しました。
pathの設定は各々の環境に応じて変更してください。
# for zsh
% curl -fsSL https://bun.sh/install | bash
% echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc
% source ~/.zshrc
% bun --version
1.1.29
How to execute each C code
Bunの動作を試す前に、nodeやdenoのFFIも動かしてみましょう。
Node.js
Nodeの場合、一般的にはN-APIやnode-gypを使用してCのコードを実行します。
今回はRustとN-APIで記述されたモジュールであるffi-rsをつかってみます。
まずは実行対象となるcのファイルを作成します。
//hello.c
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
ライブラリとしてコンパイルします。
% gcc -shared -o libhello.dylib hello.c
ffi-rsをnpmでインストールして、ライブラリを実行します。
% npm install ffi-rs
//index.js
const { open, load,close,DataType } = require('ffi-rs');
// liblaryをロード
open({
library: 'libhello', // key
path: "./libhello.dylib" // path
});
const result = load({
library: "libhello",
funcName: 'hello',
retType: DataType.Void,
paramsType: [],
paramsValue: []
})
;
close("libtest");
% node index.js
Hello from C!
実行できました。
Cのコードを直接実行するのではなく、手動でライブラリを作成してffiで実行します。
Deno
Denoでも基本的には共有ライブラリから関数を呼び出します。
Deno.dlopen() 関数を使ってライブラリ(.so .dylib .dll)をロードし、関数を実行します。
こちらでもNodeと同様、事前にコンパイルしておく必要があります。
先程のlibhello.dylibをtest.tsで呼び出してみましょう。
//test.ts
const libName = "libhello.dylib";
const dylib = Deno.dlopen(
libName,
{
"hello": { parameters: [], result: "void" }
}
);
dylib.symbols.hello();
ffi実行を許可するオプションをつけてdenoコマンドを実行します。
% deno run --allow-ffi --unstable-ffi deno-test.ts
Hello from C!
Bun
Bunの場合はNode/Denoと少し違い、内蔵のLLVMを使用してC言語コードを直接コンパイルして実行します。
下記のようcc(TinyCC)でCファイルのパスと関数の定義を指定します。
//bun-test.js
import { cc } from "bun:ffi";
const {
symbols: { hello },
} = cc({
source: "./hello.c",
symbols: {
hello: {
args: [],
returns: "void",
},
},
});
hello();
そのまま実行すればOKです。
共有ライブラリを作成する必要はありません。
% bun bun-test.js
Hello from C!
Node,Deno,BunのFFI実行方法を見てみました。
簡便性としては
Bun > Deno > Node.js
という感じです。
また、BunのffiはN-FFIよりも高速らしいです。
ではもう少しだけ複雑なサンプルをうごかしてみましょう。
Bun Example
まずCのファイルを作成します。
ここでは素数を判定するis_prime関数と
文字列を分割する関数split_string(メモリ解放用free_tokens関数も)
を定義します。
//test.c
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
// 素数判定関数
// number: 判定する整数
bool is_prime(int number) {
if (number <= 1) return false;
if (number == 2) return true;
if (number % 2 == 0) return false;
// 3から√numberまでの奇数で割り切れるかチェック
for (int i = 3; i * i <= number; i += 2) {
if (number % i == 0) return false;
}
return true;
}
// 文字列分割関数
// str: 入力文字列
// delimiter: 区切り文字
// tokens: トークンを格納する配列(事前にメモリ確保済み)
// max_tokens: トークンの最大数
// 戻り値: 実際に分割されたトークンの数
int split_string(const char* str, char delimiter, char** tokens, int max_tokens) {
if (str == NULL || tokens == NULL || max_tokens <= 0) return 0;
int count = 0;
const char* start = str;
const char* current = str;
size_t len;
while (*current != '\0') {
if (*current == delimiter) {
len = current - start;
if (len > 0 && count < max_tokens) {
// トークン用メモリを動的に割り当て
tokens[count] = (char*)malloc(len + 1);
if (tokens[count] == NULL) {
// メモリ割り当て失敗時は既に割り当てたメモリを解放
for (int i = 0; i < count; i++) {
free(tokens[i]);
tokens[i] = NULL;
}
return count;
}
// トークン文字列をコピーし、NULL終端
strncpy(tokens[count], start, len);
tokens[count][len] = '\0';
count++;
}
start = current + 1; // 次のトークンの開始位置を更新
}
current++;
}
// 最後のトークンを処理
len = current - start;
if (len > 0 && count < max_tokens) {
tokens[count] = (char*)malloc(len + 1);
if (tokens[count] == NULL) {
for (int i = 0; i < count; i++) {
free(tokens[i]);
tokens[i] = NULL;
}
return count;
}
strncpy(tokens[count], start, len);
tokens[count][len] = '\0';
count++;
}
return count;
}
// トークン解放関数
// tokens: トークン配列
// count: トークン数
void free_tokens(char** tokens, int count) {
if (tokens == NULL) return;
for (int i = 0; i < count; i++) {
if (tokens[i] != NULL) {
printf("Freeing token: %s\n", tokens[i]);
free(tokens[i]);
tokens[i] = NULL;
}
}
}
そして、Bunで実行するjsファイルを作成します。
bun:ffiのccで上記cファイルを指定し、
is_prime、split_string、free_tokensを定義します。
ポインタやダブルポインタなど、それぞれ指定方法が定義してあります。
//test-bun.js
import { cc, ptr, CString } from "bun:ffi";
const {
symbols: { is_prime, split_string, free_tokens },
} = cc({
source: "./test.c", // コンパイル対象のC source file
symbols: {
is_prime: {
args: ["int"], // 入力引数の型
returns: "bool", // 戻り値の型
},
split_string: {
args: ["cstring", "char", "ptr", "int"], // 引数の型
returns: "int", // 戻り値の型
},
free_tokens: {
args: ["ptr", "int"], // 引数の型
returns: "void", // 戻り値の型
},
},
});
// ----------------------------------------
// 1. is_prime 関数の使用例
// ----------------------------------------
let flag;
// 数値10が素数かチェック
flag = is_prime(10);
console.log("is_prime(10) =", flag);
// 数値7が素数かチェック
flag = is_prime(7);
console.log("is_prime(7) =", flag);
// ----------------------------------------
// 2. split_string 関数の使用例
// ----------------------------------------
// 入力文字列
const inputString = Buffer.from("apple,banana,cherry,dates\0", "utf-8");
console.log("before split String :",new String(inputString));
// 区切り文字 ','
const delimiter = ",".charCodeAt(0);
// 分割後の最大トークン数
const maxTokens = 10;
// トークン保存用のメモリを確保(各ポインタは8バイトと仮定)
const tokensBuffer = Buffer.alloc(maxTokens * 8);
const tokensPtr = ptr(tokensBuffer);
// split_stringを呼び出して文字列を分割
const splitCount = split_string(ptr(inputString), delimiter, tokensPtr, maxTokens);
console.log(`JavaScript : split_string() returned ${splitCount} tokens.`);
// トークンを格納する配列
const tokens = [];
// Uint64Arrayを使用してtoken pointerを使う
const bufferUint64 = new BigUint64Array(tokensBuffer.buffer, tokensBuffer.byteOffset, maxTokens);
for (let i = 0; i < splitCount; i++) {
const ptrValue = bufferUint64[i];
// NULLポインタでループを終了
if (ptrValue === 0n) {
break;
}
const ptrNumber = Number(ptrValue);
// ポインタが安全に数値として表現できるかチェック
if (!Number.isSafeInteger(ptrNumber)) {
console.warn(`Warning: Pointer at index ${i} is too large to be represented accurately as a Number.`);
continue;
}
// CStringを使用してC文字列をJavaScript文字列に変換
const str = new CString(ptrNumber).toString();
tokens.push(str);
}
// 各トークンをコンソールに表示
tokens.forEach((token, index) => {
console.log(`JavaScript : Token ${index + 1}: "${token}"`);
});
// ----------------------------------------
// 3. free_tokens 関数の使用例
// ----------------------------------------
// C側で動的に割り当てられたメモリを解放
free_tokens(ptr(bufferUint64), splitCount);
console.log("JavaScript : free_tokens() called to free memory.");
実行してみると↓のような感じです。
引数の渡し方、返り値の受け取り方の型については
気をつけましょう。
あとはC側で確保したメモリの解放も忘れずに。
% bun test-bun.js
is_prime(10) = false
is_prime(7) = true
before split String : apple,banana,cherry,dates
JavaScript : split_string() returned 4 tokens.
JavaScript : Token 1: "apple"
JavaScript : Token 2: "banana"
JavaScript : Token 3: "cherry"
JavaScript : Token 4: "dates"
Freeing token: apple
Freeing token: banana
Freeing token: cherry
Freeing token: dates
JavaScript : free_tokens() called to free memory.
Summary
今回はBunのTinyCC機能を試してみました。
Cとの連携が非常に楽で良い感じです。
まだ実験的機能ですが、正式リリースが楽しみです。