UnityでRust版の形態素解析ライブラリ「Vibrato」を動かす

初めに

形態素解析で有名なものとして、Mecabがありますがより高速に動く Rust版のVibratoを以下の記事で動かしてみました。今回は、それをUnity上で動かしていきます。

ayousanz.hatenadiary.jp

今回の記事のUnityプロジェクトは、以下のリポジトリで公開しています

github.com

Rust側は以下になります

github.com

開発環境

  • Mac (M1)
  • Unity 2022.3.4f1

UnityでRustのライブラリを動かす方法について

RustのコードをUnityで動かすには,プラグイン化してUnity側からRustのコード(C言語)を呼ぶ必要があります。 今回は Apple silicon向けにビルドをしたRustのプラグインをUnityで動かしていきます

Vibratoをプラグイン

まずはプラグイン用にRustの形態素解析のコードを作っていきます

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use vibrato::{Dictionary, Tokenizer};

#[no_mangle]
pub extern "C" fn tokenize(input: *const c_char, dict_path: *const c_char) -> *mut c_char {
    let input_str = unsafe { CStr::from_ptr(input).to_str().unwrap() };
    let dict_path_str = unsafe { CStr::from_ptr(dict_path).to_str().unwrap() };

    // 辞書ファイルのロード
    let reader = zstd::Decoder::new(std::fs::File::open(dict_path_str).unwrap()).unwrap();
    let dict = Dictionary::read(reader).unwrap();

    // トークナイザーの生成
    let tokenizer = Tokenizer::new(dict)
        .ignore_space(true).unwrap()
        .max_grouping_len(24);

    let mut worker = tokenizer.new_worker();
    worker.reset_sentence(input_str);
    worker.tokenize();

    let result: String = worker.token_iter()
        .filter(|t| {
            let words: Vec<&str> = t.feature().split(',').collect();
            let subwords: Vec<&str> = words[0].split('-').collect();
            subwords[0] == "名詞" || subwords[0] == "カスタム名詞"
        })
        .map(|t| format!("{}: {}", t.surface(), t.feature()))
        .collect::<Vec<String>>()
        .join("\n");

    CString::new(result).unwrap().into_raw()
}

#[no_mangle]
pub extern "C" fn free_string(s: *mut c_char) {
    unsafe {
        if s.is_null() { return }
        drop(CString::from_raw(s));
    };
}

またRust側の Corgo.toml は以下のように定義しています

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

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

[dependencies]
vibrato = "0.5.0"
zstd = "0.12.3"

こちらを以下でビルドをします

cargo build --release --target aarch64-apple-darwin

これによって Apple silicon向けのビルドバイナリが以下のように作成されます.

Unity側でVibratoを呼び出す

Unity側では,ビルドをした 際に生成された librust_tokenizer.dylibプラグインフォルダの中に入れます。

また ipadic-mecab-2_7_0の 辞書ファイルを ipadic-mecab-2_7_0というフォルダ名を作ってその中に入れます。

次にUnity側からバイナリを呼ぶためのコードを作成します。

using System.Runtime.InteropServices;
using UnityEngine;
using System;
using System.IO;

public class RustTokenizer : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern IntPtr tokenize(string input, string dictPath);

    [DllImport("__Internal")]
    private static extern void free_string(IntPtr str);

    [DllImport("libdl.dylib")]
    private static extern IntPtr dlopen(string fileName, int flags);

    [DllImport("libdl.dylib")]
    private static extern IntPtr dlerror();

    void Start()
    {
        string libraryPath = Path.Combine(Application.dataPath, "Plugins", "librust_tokenizer.dylib");
        Debug.Log("Attempting to load library from: " + libraryPath);

        IntPtr lib = dlopen(libraryPath, 2); // RTLD_NOW = 2
        if (lib == IntPtr.Zero)
        {
            IntPtr errPtr = dlerror();
            string errorMessage = Marshal.PtrToStringAnsi(errPtr);
            Debug.LogError("Failed to load library. Error: " + errorMessage);
            return;
        }

        Debug.Log("Library loaded successfully");
        
        string input = "本とカレーの街神保町へようこそ。";
        string dictPath = Application.dataPath + "/Plugins/ipadic-mecab-2_7_0/system.dic.zst";

        IntPtr resultPtr = tokenize(input, dictPath);
        string result = Marshal.PtrToStringAnsi(resultPtr);
        free_string(resultPtr);

        Debug.Log(result);
    }
}

こちらを適当なオブジェクトにアタッチすれば,本とカレーの街神保町へようこそ。 に対する形態素解析が実行されます。

本: 名詞,一般,*,*,*,*,本,ホン,ホン
カレー: 名詞,固有名詞,地域,一般,*,*,カレー,カレー,カレー
街: 名詞,一般,*,*,*,*,街,マチ,マチ
神保: 名詞,固有名詞,地域,一般,*,*,神保,ジンボウ,ジンボー
町: 名詞,接尾,地域,*,*,*,町,マチ,マチ
UnityEngine.Debug:Log (object)
RustTokenizer:Start () (at Assets/Scripts/RustTokenizer.cs:43)