Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Rust]OpenJtalkのbindingsを実装した #162

Closed
wants to merge 47 commits into from

Conversation

qwerty2501
Copy link
Contributor

内容

OpenJtalkのbindingsとwrapperを実装しました

関連 Issue

refs #128

その他

@PickledChair さんの実装を元に作ってみました
cmakeは外部ライブラリをそのまま出力できるような機能がなく、もとの実装のようにC++で薄いwrapperを実装しました
外部コマンドを使えばうまくできるかもしれませんが、OpenJtalkのリポジトリ側でそのCMakeLists.txtを元にrustのbindingsを生成したほうが良いかもしれません。
bindコードは更新を検知するとCI上で自動生成し、自動でPRされるようになっています

@qwerty2501 qwerty2501 changed the title OpenJtalkのbindingsとwrapperを実装した [Rust]OpenJtalkのbindingsとwrapperを実装した Jun 22, 2022
@qwerty2501 qwerty2501 force-pushed the feature/add_openjtalk branch 2 times, most recently from 81ef845 to 1ef25be Compare June 22, 2022 17:58
@qwerty2501
Copy link
Contributor Author

windowsだとlinkエラーになるなあ

@Hiroshiba
Copy link
Member

Hiroshiba commented Jun 22, 2022

良いですね!!!

windowsで動かない件は一旦保留にしてマージしても良いかもと感じました。
その場合、もしopenjtalkに依存しない部分だけでテストできれば最高そうです。

ちょっとさすがにコード量が多いので参考にお伺いしたいのですが、openjtalk部分だけ別リポジトリに分けるのはかなり手間だったりしますか 👀
(最終的にはonnxruntime-rsみたくopenjtalk-rsとして別リポジトリで管理していくのが良さそうなのですが、それを今行うほうが良いのか後回しにした方がいいのかを迷っています。)

@qwerty2501
Copy link
Contributor Author

qwerty2501 commented Jun 23, 2022

@Hiroshiba CI部分も含んでいるので手間は少しかかります
このリポジトリのためのglue codeが含まれているので少なくとも今はリポジトリは分けないようにしたほうがいいと思います。リポジトリ分けるにしてもcodeの独立性はあったほうが良いと思います。
そもそもonnxruntime-rsのリポジトリが別れているのもforkしたほうが手っ取り早そうぐらいの理由だったと思うのでvoicevox_coreでしかつかわないのであれば1から実装するものに対してリポジトリを分けるメリットはあまりなさそうに感じてます。メンテしなければならない実装量はかわらないですし、リポジトリが別れている管理コストが発生するので。

@Hiroshiba
Copy link
Member

なるほどです!

いろいろ考えたのですが、まずglue code(crates/openjtalk-sys/openjtalk/src/openjtalk.cppですよね)はrustで書くという手もあるのかなと感じました。

openjtalk-sysの役割を薄いラッパーにするのかvoicevox_core専用にするのか(rust版openjtalkを再利用しやすいようにしたいかどうか)で話が分岐しそうに思います。
個人的には、まあとりあえず再利用性は一旦考えず、PRの形のようにvoicevox_core内でメンテナンスして、実装を最速にするのが良いのかなと思いました!!

(変更行数が2000行ありますが・・・!)

@qwerty2501
Copy link
Contributor Author

qwerty2501 commented Jun 24, 2022

@Hiroshiba

いろいろ考えたのですが、まずglue code(crates/openjtalk-sys/openjtalk/src/openjtalk.cppですよね)はrustで書くという手もあるのかなと感じました。

説明がちょっと難しいのですがこのglue codeはC++で書く必要があります。
cmake側の問題でちゃんとC++のコードでopenjtalkの呼び出しをしているコードを書いていないと出力として外部ライブラリであるopenjtalkの機能が出力されないようなのです。
また、voicevoxで管理されている openjtalk からはinstallの設定記載がないため直接Rustのbindingコードを出力することができない状態になっています。

openjtalk-sysの役割を薄いラッパーにするのかvoicevox_core専用にするのか(rust版openjtalkを再利用しやすいようにしたいかどうか)で話が分岐しそうに思います。

これなのですが、本来薄いラッパーにしたいのはopenjtalk-sys crateではなく、openjtalk crateになります。
openjtalk-sys crateはonnxruntime-sysのように単純にbindingするためだけのcrateにするのが理想です。現状のglue codeがある状態は理想ではないです。
そのため理想としてはまず voicevox で管理している openjtalkのcmake設定からRustのbindingコードを出力できるようにcmake設定を変更し、そのopenjtalkからRustのbindingコードを出力できるようにしたものをopenjtalk-sys crateとしたいです。
またこれらのcrateをどこのリポジトリで管理するかは要議論ですが、 voicevoxで管理されている openjtalk で一緒に管理したほうが管理上は楽かなとは思います。他のリポジトリにしてしまうとこのopenjtalkのリポジトリを恐らくgit submoduleで参照しなくてはならないため。
もしくはcmakeのビルドスクリプトを頑張れば、openjtalk側のビルドスクリプトを変更せずかつgit submoduleに依存しない形で書けるかもしれませんが、これをできるか調べるにはかなり時間がかかりそうです

変更行数が多くて申し訳ありませんが、openjtalk-sysにあるgeneratedディレクトリ配下のものについては自動出力されたものなので無視していただいて問題ないと思います。それ以外についてはそこまで行数はないのでレビューしやすいかと思います

@Hiroshiba
Copy link
Member

レビューが停滞しちゃってすみません、一旦判断だけコメントします!

なるほどです、openjtalkを外に出すのは現状だと大変ということなんですね!
理想的にはopenjtalk-sysを外に出したいとは思いつつ、いったんマージの方向で進めればと思います!

openjtalk-sysのあるべき場所としては、余力があればopenjtalkリポジトリに、更に気合があれば新しいリポジトリにあると良いのかなという所感です。

Copy link
Member

@Hiroshiba Hiroshiba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!!!

しっかり見てないのでwrapper部分とかもしかしたら実装足りないかもですが、まああとで足りないとこを足すのが良いのかなと思いました!
@PickledChair さんも見て頂けると心強いです!!

.github/workflows/gen_bind.yml Show resolved Hide resolved
.github/workflows/gen_bind.yml Outdated Show resolved Hide resolved
@qwerty2501
Copy link
Contributor Author

qwerty2501 commented Jun 25, 2022

@Hiroshiba 念の為TODOで将来的にopenjtalk (openjtalk-sysではなく) を薄いwrapperにする旨のTODOを記載しておきました
やりとりが長かったのでそちらが意図してない内容かもしれないのでその場合は指摘お願いします。(そもそもいらなかったですかね?)
beeacc9

Copy link
Member

@PickledChair PickledChair left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

いくつか解決の必要な問題がありそうだったのでコメントしました(自分が埋め込んでいた問題もあります、すみません……!)。確認よろしくお願いいたします……!

@@ -0,0 +1,36 @@
fn main() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

openjtalk-sys と openjtalk クレートをビルドする分にはこの build.rs の記述でビルドが通るのですが、C++ コードを用いている関係で、最終的に共有ライブラリをビルドする時にはリンクのために -lstdc++ 等のコンパイラフラグが必要になります。

この build.rs に直接記述するのが普通の方法だと思うのですが、自分が実装したときは面倒に思い、 https://github.com/dtolnay/link-cplusplus を見つけたので使いました。これは lib.rs

extern crate link_cplusplus;

と記述するだけで、このクレートの build.rs に記載されている C++ 標準ライブラリのリンクのためのコンパイラフラグが追加されるようになる、というものです。これを用いても良いと思いますし、これなしで自前でコンパイラフラグを書くのも良いと思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

とりあえず入れてみました

cmake_minimum_required(VERSION 3.16)
project(OpenjtalkSys)

include(FetchContent)
Copy link
Member

@PickledChair PickledChair Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#121 (comment) で FetchContent の機能について触れられていたので、自分も気になっていたのですが使い方がわかっておりませんでした。実例を見ることができて勉強になります!

ただ、試しにビルドされた openjtalk クレートを voicevox_core 側から使ってみようとすると、ビルド時にリンクエラーが発生しました:

$ cargo build

... 省略 ...

error: linking with `cc` failed: exit status: 1

... 中略 ...

  = note: Undefined symbols for architecture x86_64:
            "_Mecab_initialize", referenced from:
                (anonymous namespace)::OpenJTalk::OpenJTalk() in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
            "_NJD_initialize", referenced from:
                (anonymous namespace)::OpenJTalk::OpenJTalk() in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
            "_JPCommon_initialize", referenced from:
                (anonymous namespace)::OpenJTalk::OpenJTalk() in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
            "_Mecab_clear", referenced from:
                (anonymous namespace)::OpenJTalk::clear() in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
            "_NJD_clear", referenced from:
                (anonymous namespace)::OpenJTalk::clear() in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
            "_JPCommon_clear", referenced from:
                (anonymous namespace)::OpenJTalk::clear() in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
            "_Mecab_load", referenced from:
                (anonymous namespace)::OpenJTalk::load(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) in libopenjtalk_sys-65082b9991f752de.rlib(openjtalk.cpp.o)
          ld: symbol(s) not found for architecture x86_64
          clang: error: linker command failed with exit code 1 (use -v to see invocation)

open_jtalk 内で定義されていると思われる幾つかのシンボルが未定義になっていました。どういうことかと思って、コンパイルされた libOpenjtalkSys.a 内のシンボルを調べてみたところ、上記のエラーで出ていたシンボルは以下のように確かに未定義になっていました:

$ nm libOpenjtalkSys.a

libOpenjtalkSys.a(openjtalk.cpp.o):
0000000000000a5c s GCC_except_table0
0000000000000a6c s GCC_except_table2
0000000000000aa8 s GCC_except_table26
0000000000000a7c s GCC_except_table5
0000000000000a98 s GCC_except_table6
                 U _JPCommon_clear
                 U _JPCommon_get_label_feature
                 U _JPCommon_get_label_size
                 U _JPCommon_initialize
                 U _JPCommon_make_label
                 U _JPCommon_refresh
                 U _Mecab_analysis
                 U _Mecab_clear
                 U _Mecab_get_feature
                 U _Mecab_get_size
                 U _Mecab_initialize
                 U _Mecab_load
                 U _Mecab_refresh
                 U _NJD_clear
                 U _NJD_initialize
                 U _NJD_refresh
0000000000000560 T _OpenJTalk_clear
0000000000000000 T _OpenJTalk_create
00000000000005c0 T _OpenJTalk_delete
00000000000000a0 T _OpenJTalk_extract_fullcontext
00000000000003f0 T _OpenJTalk_load
... 以下省略 ...

自分の以前の実装 https://github.com/PickledChair/voicevox-tts-rs では問題がなかったので、そちらがなぜ大丈夫だったのか調べたところ、libOpenjtalkSys.a にあたる静的ライブラリの他に、open_jtalk 自体の静的ライブラリ libopenjtalk.a が生成物として存在しており、そちらに該当のシンボルが定義されていました。

FetchContent の機能に詳しくないので、解決方法がわかりません……。もし FetchContent に元の open_jtalk のコードを静的ライブラリにする機能がない場合は、git submodule を使う方法を使わざるを得ないのかもしれない、と考えました。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これについては直接参照できるようになったので解決できました

Comment on lines 56 to 63
let labels_ptr = OpenJTalk_extract_fullcontext(
self.ptr,
text.as_ptr(),
(&mut extract_size) as *mut usize,
);
for ptr in std::slice::from_raw_parts(labels_ptr, extract_size) {
let c_str = CString::from_raw(*ptr);
result.push(c_str.to_str().unwrap().to_string());
Copy link
Member

@PickledChair PickledChair Jun 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

元のコードを書いた自分が悪い(後で気づきました……)のですが、この部分は以下の2点において良くないやり方になってしまっています:

  • labels_ptr で参照されている配列は C++ のコード側で malloc によって動的確保されたものなので、メモリリークが発生してしまっている
  • CString::from_raw メソッドで C++ 側でメモリ確保した文字列を扱おうとしているが、 https://doc.rust-lang.org/std/ffi/struct.CString.html#method.from_raw によるとこのメソッドは、Rust 側で確保された文字列データの所有権を取り戻すために使うことを想定したもの(引数となるポインタは Rust 側で確保されたメモリ領域を参照するポインタであってほしい)であり、Rust の外部で確保された文字列データのポインタを引数にとった上での動作は未定義動作扱いのようである

これらを考え合わせると、代わりに

  1. void OpenJTalk_free_labels(char **labels_ptr, int labels_size) のようなメモリ解放用の関数を C++ コード側に用意(labels の配列内の各文字列ポインタについて文字列データを free した後、labels 自体の配列データを free するような関数)
  2. Rust 側で取得した labels_ptr から各文字列を取得するには CString::from_raw ではなく CStr::from_ptr を用いて、そこから to_owned メソッドで文字列データをコピーする
  3. 1. で定義した関数を用いて labels_ptr が参照しているヒープ領域を解放

という方法をとるのが最も素直な実装方法かと思います。メモリコピーの回数が増えてしまっているので、もしもっとスマートな方法が思いついた場合はご提案ください……!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapperから書き直しになったので解消されると思います

@qwerty2501 qwerty2501 force-pushed the feature/add_openjtalk branch 9 times, most recently from f73f7bd to 56af26f Compare June 26, 2022 10:08
@qwerty2501
Copy link
Contributor Author

問題ないと思います!コメントアウトしちゃいましょう。

別のリポジトリですが誰がやりますか?

ただ今この瞬間だと差分が78万行あるので、流石にどうにかしたほうが良いのかも・・・と思いました!
(openjtalkのテスト用のコードが全部吐き出されてそう?)

これは openjtalkのヘッダ内でC標準ライブラリのFILE構造体を使ってるのですが、出力する際にこの型が何かわからなくなるので stdio.hも出力対象に含めているためですね。(stdio.hは各プラットフォーム依存のheaderをincludeしているためbindgenで出力する際にそれらすべてが出力されます。一応出力内容をある程度制御できるのですが必要なものだけとなるとかなり手間がかかる上、どのみち最終的な成果物からは使っていないものは取り除かれるのでこのようになっています)
またbindgenはheaderから定義を出力するだけなのでopenjtalkのテストコードが出力されているようなことはまずないと思われます。

ただ流石に量が多いので、すこし時間がかかりますがリポジトリ分けますか?

@qwerty2501
Copy link
Contributor Author

@Hiroshiba とりあえずopen_jtalkのほうにPR出してみました

@Hiroshiba
Copy link
Member

stdio.hも出力対象に含めているため

あー、なるほどです!!
ヘッダーからFILEを省いちゃいたくなる気もしますが・・・この辺あまりいじりたくないですね・・・!

テストコード(?)、こんな感じでした。
といっても20~30万行くらいなので、桁数はあまり変わらなそう。
image

qwerty2501 and others added 10 commits June 26, 2022 17:54
…_bindings_x86_64-unknown-linux-gnu

Automated generate bindings for x86_64-unknown-linux-gnu
…_bindings_x86_64-apple-darwin

Automated generate bindings for x86_64-apple-darwin
…_bindings_i686-pc-windows-msvc

Automated generate bindings for i686-pc-windows-msvc
…_bindings_aarch64-apple-darwin

Automated generate bindings for aarch64-apple-darwin
…_bindings_x86_64-pc-windows-msvc

Automated generate bindings for x86_64-pc-windows-msvc
@qwerty2501
Copy link
Contributor Author

qwerty2501 commented Jun 26, 2022

@Hiroshiba include順を変えたらwrapper.hppから stdio.h を消すことには成功したのですが、恐らくopenjtalk内部のheaderのどこかで stdio.hをincludeしてるので結局生成されるコード量削減にはつながらなかったです
また出力されているテストコードですが、これはopenjtalkのテストコードではなくbindされたコードが呼び出せるか確認する目的でbindgenが自動生成したものだと思います。
これはオプションにより生成しないようにはできますが、テストコードなので最終的な成果物には悪影響をあたえないため残しておいたほうが良いと思います

前のコメント でさすがにコード量が多いのでリポジトリ分けるか質問していたのですがどうしましょうか?

@Hiroshiba
Copy link
Member

Hiroshiba commented Jun 27, 2022

openjtalkのテストコードではなくbindされたコードが呼び出せるか確認する目的でbindgenが自動生成したもの

おお、なるほどです!
openjtalkのテストがコピーされてる感じだったら不要かなと思ったのですが、それなら残っていても良さそうに思います。

コード量が多いのでリポジトリ分けるか

すみません、お答えしてませんでした!
分けられるならぜひ分けたいなという感じです!!VOICEVOX貢献のためにpullしたら数十万行降ってくるのはちょっとした障壁になりえそうなので・・・。

方法はおまかせできればと思っています。
提案としては、onnxruntime-sysみたく一旦qwertyさんのリポジトリで運用して頂き、動くとこまで迅速に進んであとでVOICEVOXに組み込む、というが良いのかなと感じています。
(もちろん最初からVOICEVOXでメンテし、みんなでPRを見るということも可能です)

@qwerty2501
Copy link
Contributor Author

@Hiroshiba

分けられるならぜひ分けたいなという感じです!!VOICEVOX貢献のためにpullしたら数十万行降ってくるのはちょっとした障壁になりえそうなので・・・。

では分ける方向ですかね。onnxruntimeのようにwrapperも分けたリポジトリ先で管理するということでいいですかね?

@Hiroshiba
Copy link
Member

Hiroshiba commented Jun 27, 2022

onnxruntimeのようにwrapperも分けたリポジトリ先で管理する

こちらの処理に該当するコードですよね。
https://github.com/VOICEVOX/voicevox_core/blob/c47d05576e04360735c29de3e4fc73424440c48c/core/src/engine/openjtalk.cpp
VOICEVOX専用のロジックでも無いですし、wrapperもそちらで良いかなと思います。
(可能であれば、追加実装しやすいのでVOICEVOX core側にあったほうが嬉しそうではあります。)

@qwerty2501
Copy link
Contributor Author

qwerty2501 commented Jun 27, 2022

@Hiroshiba すみませんそこではないです。
openjtalk-sysで生成したコードだとポインタだったりとかを渡さないといけないのでそのあたりをRustぽく呼び出さるようにするだけを目的としたwrapperですね
なのでリンク先相当のコードはvoicevox_coreで管理することになると思います

@Hiroshiba
Copy link
Member

あ!勘違いしました、onnxruntime-rsにおけるonnxruntime-sys crateとonnxruntime crateに該当するとこを別リポジトリに、↑のコードはVOICEVOX coreに、ということですね!!一番良いと思います・・・!!

@qwerty2501
Copy link
Contributor Author

リポジトリ作成したのでcloseします

@qwerty2501 qwerty2501 closed this Jun 27, 2022
@Hiroshiba
Copy link
Member

あ、ちなみにPickledChair さんからのコメントは反映されている感じでしょうか 👀
せっかくのコメントなので見逃しがあるともったいないなと・・・!

@qwerty2501
Copy link
Contributor Author

概ね大丈夫だと思います。
とりあえずコメントは返しておきました

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants