2024年3月29日金曜日

ttyd の main 関数を Rust プログラムから呼び出す(Rust から C の関数を呼び出す)

開発環境の準備

開発用コンテナの起動

docker run -it --rm --workdir /work -p 0.0.0.0:7681:7681 rust:1.77.0-slim-bookworm bash
WORKDIR=/work

ttyd のソースコード取得

git インストール。

apt update
apt install -y git

ttyd のリポジトリを clone.

cd $WORKDIR
git clone --depth 1 -b 1.7.5 https://github.com/tsl0922/ttyd

プロジェクトディレクトリへ移動。

cd $WORKDIR/ttyd

ttyd をライブラリとしてビルド

ttyd の README を参照しながら、ビルドに必要なパッケージをインストールする。

apt install -y build-essential cmake git libjson-c-dev libwebsockets-dev

前回の apt install と重複があるが、気にせず ttyd の README からそのままコピペ。

CMakeLists.txt を修正

実行バイナリではなく、 .a を生成するように CMakeLists.txt を修正する。

sed -i -e 's/add_executable(${PROJECT_NAME} ${SOURCE_FILES})/add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES})/' ./CMakeLists.txt
  • add_executable: 実行バイナリを生成する
  • add_library: .a を生成する

関数 main の名前変更

別プログラムに組み込みたいので、エントリーポイントである main が存在すると困る。

なので、 mainttyd_main へリネームする。

sed -i -e 's/main(/ttyd_main(/' ./src/server.c

別プログラムから、 ttyd_main(2, ["ttyd", "-W", "bash"]) のような感じで呼び出すイメージ。

ttyd のビルド

mkdir build && cd build
cmake ..
make

これで、 libttyd.a が生成される。

ttyd を組み込む Rust プログラムを作る

ttydwrapper というプロジェクトを作って、 固定値で ttyd -W -t enableSixel=true bash コマンドを実行するのと同じように ttyd を呼び出すプログラムを作る。

bindgen に必要なパッケージのインストール

C 言語との FFI をするにあたり、 bindgen というコマンドを利用するので、それに必要なパッケージをインストールする。

apt update
apt install -y libclang-16-dev

Cargo プロジェクトの作成

今回は、 ttydwrapper という名前で作成。

cd $WORKDIR
cargo new ttydwrapper
cd ttydwrapper
rustup component add rustfmt

FFI 用のヘッダーファイルを作成

Rust から C の ttyd_main を呼び出すので、 ttyd_main の定義が記載されているヘッダーファイルを作成する。

Rust では、一般的に、 binding.h に必要なヘッダーファイルを列挙するらしい。

なので今回は、 ttyd.httyd_main を定義し、そのヘッダーファイルを binding.h へ記載する構成でヘッダーを作成した。

mkdir include
cat << EOF > ./include/ttyd.h
void ttyd_main(int argc, char **argv);
EOF

cat << EOF > ./include/binding.h
#include "ttyd.h"
EOF

バインディングを生成

bindgen コマンドで、ヘッダーから Rust のコードを生成する。

cargo install bindgen-cli
bindgen ./include/binding.h > ./src/bindings.rs

以下のような、 binding.rs が生成される。

/* automatically generated by rust-bindgen 0.69.4 */

extern "C" {
    pub fn ttyd_main(argc: ::std::os::raw::c_int, argv: *mut *mut ::std::os::raw::c_char);
}

生成された binding.rslib.rs からインクルードさせるのが一般的らしいので libs.rs を作る。

cat << EOF > ./src/lib.rs
include!("bindings.rs");
EOF

ライブラリをコピー

ライブラリは lib 以下に無いとダメなので .a を移動する。

mkdir $WORKDIR/ttydwrapper/lib
cp $WORKDIR/ttyd/build/libttyd.a $WORKDIR/ttydwrapper/lib/

ライブラリの情報を作成

build.rs にビルド時に必要なライブラリ情報の列挙をする。

cat << EOF > ./build.rs
fn main() {
    println!("cargo:rustc-link-search=native=$WORKDIR/ttydwrapper/lib");
    println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu");
    println!("cargo:rustc-link-lib=static=ttyd");
    println!("cargo:rustc-link-lib=dylib=uv");
    println!("cargo:rustc-link-lib=dylib=websockets");
    println!("cargo:rustc-link-lib=static=ssl");
    println!("cargo:rustc-link-lib=static=crypto");
    println!("cargo:rustc-link-lib=static=z");
    println!("cargo:rustc-link-lib=static=json-c");
}
EOF

ビルド時に build.rs の設定を使用するように修正

Cargo.toml[package] の末尾に build = "build.rs" を追加する。

(sed で実現できなかったので vim で編集した)

main 関数の作成

Rust の main 関数を作成し、 ttyd_main を呼び出すプログラムを作る。

cat << EOF > ./src/main.rs
use std::ffi::CString;
use std::os::raw::c_char;
use ttydwrapper::ttyd_main;

fn main() {

    let args = vec!["tty", "-W", "-t", "enableSixel=true", "bash"];

    let c_args: Vec<CString> = args.into_iter()
        .map(|arg| CString::new(arg).expect("CString::new failed"))
        .collect();

    let mut argv: Vec<*mut c_char> = c_args.iter()
        .map(|arg| arg.as_ptr() as *mut c_char)
        .collect();

    let argv_ptr: *mut *mut c_char = argv.as_mut_ptr();

    unsafe {
        ttyd_main(5, argv_ptr);
    }
}
EOF

ビルド

cargo build

これで ./target/debug/ttydwrapper が生成される。

動作確認

コンテナ起動時に 7681 をポートフォワーディングしているので、 ttydwrapper を実行した後 http://localhost:7681 へアクセスすると ttyd のターミナルが開く。

以上。

ここまで書いたところで、本当にやりたいのは Windows でだった事に気付いた…。

参考資料

0 件のコメント:

コメントを投稿