[Bun] JavaScriptでC言語のファイルを簡単に実行する

[Bun] JavaScriptでC言語のファイルを簡単に実行する

Clock Icon2024.09.26

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との連携が非常に楽で良い感じです。
まだ実験的機能ですが、正式リリースが楽しみです。

References

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.