⚙️

DenoからFFIでRustを呼び出す

2022/12/07に公開約2,400字2件のコメント

FFIというものを使ったことがなかったので、お試しでDenoからRustの関数を呼び出してみます。

FFIとは

Foreign Function Interface の略で、あるプログラミング言語で書かれたプログラムから、別のプログラミング言語で書かれた関数を呼び出すためのインタフェースです。

プロジェクトの作成

mkdir deno-rust-ffi
cd deno-rust-ffi

ライブラリの作成

今回はライプニッツの公式で円周率を求める関数を作って、せっかくなのでdenoで実装した場合との速度の比較をしてみようかと思います。

cargo init --lib

Cargo.tomlを編集する

[package]
name = "myfirstffi"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]

lib.rsを編集する

#[no_mangle]
pub extern "C" fn leibniz(n: u32) -> f64 {
    let mut pi = 0.0;
    for i in 0..(n+1) {
        pi += (-1i32).pow(i) as f64 /(2*i+1) as f64;
    }
    pi * 4.0
}

#[no_mangle]とは

Rustのアトリビュート(属性)の1つです。
Rustでは、同じ名前の関数が異なるライブラリから定義されることを防ぐために、人間に読みにくい異なる名前に変えるマングリングというのが行われます。
これが行われてしまうとFFIで関数を参照する際に見つからなくなってしまうので、#[no_mangle]でマングリングが行われないようにします。

ライブラリのビルド

cargo build --release 

./target/release/フォルダの中にlibfirstffi.dylibというのができているはずです。
firstffiの部分はCargo.tomlで名付けた名前になり、拡張子の.dylibは環境によって違うので注意。

ls ./target/release
build
deps
examples
incremental
libmyfirstffi.d
libmyfirstffi.dylib

denoでライブラリを読み込む

main.tsを作成する。

// 環境ごとに違う拡張子に対応する。
let libSuffix = "";
switch (Deno.build.os) {
  case "windows":
    libSuffix ="dll";
    break;
  case "darwin":
    libSuffix ="dylib";
    break;
  default:
    libSuffix ="so";
    break;
}
// ライブラリを読み込む
const library = Deno.dlopen(`./target/debug/libmyfirstffi.${libSuffix}`,  {
  leibniz: {
    parameters: ["u32"], // 引数の型
    result: "f64",             // 戻り値の型
  }
})

const result = library.symbols.leibniz(100000);
console.log(result);

実行します。
--allow-ffiでffiの使用を許可し、--unstableフラッグが必要です。

deno run --allow-ffi --unstable main.ts

無事円周率を求めることができました。

3.1416026534897203

速度の比較

main.tsに次のように書き加えて速度を比較してみます。

function deno_leibniz(n: number) {
  let pi = 0;
  for (let i = 0; i < n + 1; i++) {
    pi += Math.pow(-1, i) / (2 * i + 1);
  }
  return pi * 4;
}

console.time("deno_leibniz");
const d = deno_leibniz(1000000);
console.timeEnd("deno_leibniz");
console.log(d);

console.time("rust_leibniz");
const r = library.symbols.leibniz(1000000);
console.timeEnd("rust_leibniz");
console.log(r);

結果!

❯ deno run --allow-ffi --unstable main.ts
deno_leibniz: 12ms
3.1415936535887745
rust_leibniz: 116ms
3.1415936535887745

あれ?denoの方が10倍も早い...?
なんとも締まらない結果になりましたが、FFIの使い方を知りたかったのでとりあえず良しとします。
自分でもまた調べてみますが、denoの方が早かった理由を知っている方は教えていただけると嬉しいです。

Discussion

cargo buildではなくcargo build --releaseでビルドして、target/debug以下ではなくてtarget/release以下の共有ライブラリを呼び出すべきです

ご指摘ありがとうございます。
修正しました。

ログインするとコメントできます