🌏

Rustを中心に世界は回る ~Rustで実装したモジュールを異なる言語から呼び出してみた~

こんにちは、エンジニアの籏野です。
フォルシアでは数年前からRustに関連したイベント(Shinjuku.rs)を開催したり、本の執筆を行ったりとRustに関する活動が行われてきました。
さらにここ最近はRustをよりコアな部分に活用していけないかという議論も活発に行われており、Rustは今後の開発でより重要な役割を担っていくのではないかと感じています。

私自身はこれまでRustにしっかりと触れる機会がなかったのですが、先日プロダクトの検討の一部としてRustを利用できないかという話が出たので、実際にRustを使った処理を実装してみることにしました。
今回の記事ではその際に試したことをまとめていきたいと思います。

やったこと

私が担当するあるプロダクトで、「webアプリでも、データ処理でも同じようなロジックを使いまわしたい」という要望が上がりました。
このプロダクトでは、webアプリはNode.js、データ処理にはPythonを利用しているのですが、それぞれの言語で同じようなロジックを実装するのはコストがかかるため、別の言語で実装してそれを両方のシステムから呼び出せないかという話になりました。
以前からWebAssemblyをRustで書いた等の記事が気になっていたこともあり、今回の機会に実際にRustを利用した実装を行うことにしました。

実際のプロダクトのコードはお見せできないので、以降の説明では「四則演算の構文解析器」を対象のロジックとして扱います。(異なる言語でそれぞれ実装するには面倒ですよね)
「四則演算の構文解析器」自体についてはRust でつくるインタプリタという記事をほぼそのまま参考にさせていただいたので、構文解析について気になる方はそちらをご覧ください。
本記事では、このロジックをNode.js/Pythonそれぞれから呼び出す際に工夫した点を紹介します。

成果物については以下のリポジトリにまとめていますので、合わせてご確認ください。
https://github.com/taku-hatano/rust_exercise

なお本記事の内容は以下の環境にて動作を確認しています。

Rust	1.69.0
Python	3.11.3
Node.js	18.16.0

また、それぞれの言語へのバインディングには以下のツールを利用していますので、適宜インストールしてください。

ポイント1: 他言語からのエントリーポイントを作成

今回作成したプロジェクトのディレクトリ構成は以下のようにしました。

./src
├── external_module
│  ├── nodejs
│  │  ├── lexer.rs
│  │  └── parser.rs
│  ├── python
│  │  ├── lexer.rs
│  │  └── parser.rs
│  ├── nodejs.rs
│  └── python.rs
├── external_module.rs
├── lexer.rs
├── lib.rs
├── main.rs
├── parser.rs
└── token.rs

ポイントとなるのはexternal_moduleディレクトリで、ここに他言語から呼び出すためのエントリーポイントとなるファイルを置いています。
他言語とのバインディングツールの例を見ていると、実装部分と各言語のモジュールに変換するための記載が同じファイルに書かれていることが多いように感じました。
プリミティブな値をRust ⇔ 他言語間でやり取りするような簡単な実装であればこの方法でもよさそうですが、今回のように複雑なオブジェクトをやり取りする必要がある場合には別ファイルに分けておいた方が良いと感じました。
これにより、コアな実装部分はRustのみを気にすればよくなり余計なことを考える必要がなくなりました。

ポイント2: Rust⇔他言語への変換ロジックを記載

今回作成したモジュールはTokenやExpressionのようなRust上で定義された構造体を返却します。
PythonやNode.jsでこれらの構造体を扱いたい場合、それぞれの言語に合わせて構造体を変換する処理を記載する必要があります。
例えばTokenをPythonのクラスに変換する場合は以下のようなコードになります。

https://github.com/taku-hatano/rust_exercise/blob/main/src/external_module/python/lexer.rs#L44-L56

pyo3がPythonのクラスに変換するためのトレイトを提供してくれているので、それを利用した変換を行っています。
NumberTokenやOperatorTokenもpyo3を利用して定義したクラスになっており、Pythonでこのモジュールを呼び出した場合、以下のような判定が可能になります。
Rustの構造体がPythonのクラスに変換され、Python上で違和感なく扱えており感動しました。

from rust_exercise import PyLexer, NumberToken

lexer = PyLexer("1")
token = lexer.token()

if isinstance(token, NumberToken):
	print(f"{token.value} is number.")

Node.js(wasm)の場合も同様の考え方で実装が可能となっています。
https://github.com/taku-hatano/rust_exercise/blob/main/src/external_module/nodejs/lexer.rs#L31-L53

ポイント3: featuresフラグの利用

何も設定せずにビルドを行うと無駄に時間がかかっているように感じたので、以下のようにfeaturesフラグを設定し、ビルド時に必要な機能のみを有効にしてみました。

// src/exercise_module.rs
#[cfg(feature = "python")]
mod python;

#[cfg(feature = "nodejs")]
mod nodejs;

またMakefileを用意して、各featuresフラグを指定し実装者がフラグをあまり意識せずにすむような形を目指しています。

https://github.com/taku-hatano/rust_exercise/blob/main/Makefile

実際に他言語から呼び出してみよう!

それぞれの言語で四則演算の解析を行い実際に計算するインタプリタを実装したので、実行結果を見てみましょう。
Lexerが返すTokenがそれぞれの言語に合わせて変換した値になっていることや、式の評価を正しく行い結果が一致していることが確認できます。

Rust

$ cargo run
[rust]>> 1+2*(-3+1)

**** Lexer Result ****
Some(Number(1.0))
Some(Plus)
Some(Number(2.0))
Some(Asterisk)
Some(LParen)
Some(Minus)
Some(Number(3.0))
Some(Plus)
Some(Number(1.0))
Some(RParen)

**** Evaluate Result ****
-3

Python

※ 実行前にPython用のビルドを行った後にpip install .等でライブラリをインストールしてください。

$ python3 ./main.py
[python]>> 1+2*(-3+1)
**** Lexer Result ****
<builtins.NumberToken object at 0x7fd5f0253a90>
<builtins.OperatorToken object at 0x7fd5f0253c50>
<builtins.NumberToken object at 0x7fd5f0253a90>
<builtins.OperatorToken object at 0x7fd5f0253c50>
<builtins.OperatorToken object at 0x7fd5f0253a90>
<builtins.OperatorToken object at 0x7fd5f0253c50>
<builtins.NumberToken object at 0x7fd5f0253a90>
<builtins.OperatorToken object at 0x7fd5f0253c50>
<builtins.NumberToken object at 0x7fd5f0253a90>
<builtins.OperatorToken object at 0x7fd5f0253c50>

**** Evaluate Result ****
-3.0

Node.js

$ node ./main.js
[nodejs]>> 1+2*(-3+1)
**** Lexer Result ****
{ Number: 1 }
{ Operator: 'Plus' }
{ Number: 2 }
{ Operator: 'Asterisk' }
{ Operator: 'LParen' }
{ Operator: 'Minus' }
{ Number: 3 }
{ Operator: 'Plus' }
{ Number: 1 }
{ Operator: 'RParen' }

**** Evaluate Result ****
-3

まとめ

ということで、無事にRustで実装したモジュールを異なる複数の言語から呼び出すことができました。
私は「構造体の変換を行わなければならない」という点に中々気が付くことができず、実装に時間がかかってしまいました。
また実装とバインディングを分けたことで全体の見通しがよくなったと感じており、同じようなことを行う方の参考になれば幸いです。

ある程度複雑な構造体を使っていても、Rustで実装したモジュールをNode.jsでもPythonでも呼び出すことができることが今回の実装を通してわかりました。
近い将来、Webアプリでもデータ処理でもRustを中心とした世界観で開発が回っていくこともあり得るのではないでしょうか。

この記事を書いた人

籏野 拓
2018年新卒入社

FORCIA Tech Blog

Discussion