From 5a56580be319784e9f03229e7a830067e6a452af Mon Sep 17 00:00:00 2001 From: Ryo Yamashita Date: Sun, 6 Oct 2024 02:22:43 +0900 Subject: [PATCH] =?UTF-8?q?add:=20C=20API=E3=81=AE"delete"=E3=81=AB?= =?UTF-8?q?=E3=82=BB=E3=83=BC=E3=83=95=E3=83=86=E3=82=A3=E3=83=8D=E3=83=83?= =?UTF-8?q?=E3=83=88=E3=82=92=E5=BC=B5=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C APIにおいて次の状況に対してセーフティネットを張り、パニックするように する。 1. オブジェクトが他スレッドでアクセスされている最中に"delete"を試みる 2. "delete"後に他の通常のメソッド関数の利用を試みる 3. "delete"後に"delete"を試みる このPRは #836 の解決**ではなく**、ドキュメントにも手を加えていない。とい うのも`VoicevoxVoiceModelFile`には次のゲッターメソッドがあり、これらをカ バーするのは現状のAPIの形だと不可能だからである。 * `voicevox_voice_model_file_id` * `voicevox_voice_model_file_get_metas_json` --- Cargo.lock | 100 +++++---------- Cargo.toml | 2 + crates/voicevox_core_c_api/Cargo.toml | 6 +- crates/voicevox_core_c_api/src/c_impls.rs | 120 +++++++++++++++--- crates/voicevox_core_c_api/src/lib.rs | 116 ++++++++--------- .../tests/e2e/snapshots.toml | 24 ++++ .../tests/e2e/testcases.rs | 4 + .../e2e/testcases/double_delete_openjtalk.rs | 61 +++++++++ .../testcases/double_delete_synthesizer.rs | 85 +++++++++++++ .../e2e/testcases/double_delete_user_dict.rs | 46 +++++++ .../double_delete_voice_model_file.rs | 58 +++++++++ 11 files changed, 478 insertions(+), 144 deletions(-) create mode 100644 crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_openjtalk.rs create mode 100644 crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_synthesizer.rs create mode 100644 crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_user_dict.rs create mode 100644 crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_voice_model_file.rs diff --git a/Cargo.lock b/Cargo.lock index 2c2be4f8d..3906cd6b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,7 +366,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -440,6 +440,12 @@ dependencies = [ "piper", ] +[[package]] +name = "boxcar" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba19c552ee63cb6646b75e1166d1bdb8a6d34a6d19e319dec88c8adadff2db3" + [[package]] name = "bstr" version = "1.2.0" @@ -1369,6 +1375,15 @@ dependencies = [ "miniz_oxide 0.8.0", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1872,15 +1887,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1994,7 +2000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.48.0", + "windows-targets 0.52.6", ] [[package]] @@ -2226,6 +2232,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2747,7 +2759,10 @@ checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", + "float-cmp", + "normalize-line-endings", "predicates-core", + "regex", ] [[package]] @@ -4356,15 +4371,18 @@ dependencies = [ "anstyle-query", "anyhow", "assert_cmd", + "boxcar", "camino", "chrono", "clap 4.5.19", "colorchoice", "const_format", "cstr", - "derive-getters", + "derive_more", "duct", + "duplicate", "easy-ext", + "indexmap 2.6.0", "inventory", "itertools 0.10.5", "libc", @@ -4372,6 +4390,7 @@ dependencies = [ "libtest-mimic", "ndarray", "ndarray-stats", + "predicates", "process_path", "ref-cast", "regex", @@ -4702,21 +4721,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4739,12 +4743,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4763,12 +4761,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4787,12 +4779,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4817,12 +4803,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4841,12 +4821,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4859,12 +4833,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4883,12 +4851,6 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 627fab02f..aaf8185ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ async_zip = "=0.0.16" bindgen = "0.69.4" binstall-tar = "0.4.42" blocking = "1.6.1" +boxcar = "0.2.6" bytes = "1.7.2" camino = "1.1.9" cargo_metadata = "0.18.1" @@ -60,6 +61,7 @@ once_cell = "1.20.1" ouroboros = "0.18.4" parse-display = "0.8.2" pollster = "0.3.0" +predicates = "3.1.2" pretty_assertions = "1.4.1" proc-macro2 = "1.0.86" pyo3 = "0.20.3" diff --git a/crates/voicevox_core_c_api/Cargo.toml b/crates/voicevox_core_c_api/Cargo.toml index e03d6cc4f..c51f01442 100644 --- a/crates/voicevox_core_c_api/Cargo.toml +++ b/crates/voicevox_core_c_api/Cargo.toml @@ -20,12 +20,14 @@ link-onnxruntime = ["voicevox_core/link-onnxruntime"] [dependencies] anstream = { workspace = true, default-features = false, features = ["auto"] } anstyle-query.workspace = true +boxcar.workspace = true camino.workspace = true chrono = { workspace = true, default-features = false, features = ["clock"] } colorchoice.workspace = true const_format.workspace = true cstr.workspace = true -derive-getters.workspace = true +derive_more.workspace = true +duplicate.workspace = true easy-ext.workspace = true itertools.workspace = true libc.workspace = true @@ -45,10 +47,12 @@ clap = { workspace = true, features = ["derive"] } duct.workspace = true easy-ext.workspace = true inventory.workspace = true +indexmap = { workspace = true, features = ["serde"] } libloading.workspace = true libtest-mimic.workspace = true ndarray.workspace = true ndarray-stats.workspace = true +predicates.workspace = true regex.workspace = true serde = { workspace = true, features = ["derive"] } serde_with.workspace = true diff --git a/crates/voicevox_core_c_api/src/c_impls.rs b/crates/voicevox_core_c_api/src/c_impls.rs index 0e9ff9a78..2806db66f 100644 --- a/crates/voicevox_core_c_api/src/c_impls.rs +++ b/crates/voicevox_core_c_api/src/c_impls.rs @@ -1,11 +1,20 @@ -use std::{ffi::CString, path::Path}; +use std::{ + collections::HashMap, + ffi::CString, + fmt::Debug, + ops::DerefMut, + path::Path, + sync::{Arc, LazyLock}, +}; use camino::Utf8Path; +use duplicate::duplicate_item; +use easy_ext::ext; use ref_cast::ref_cast_custom; use voicevox_core::{InitializeOptions, Result, VoiceModelId}; use crate::{ - helpers::CApiResult, OpenJtalkRc, VoicevoxOnnxruntime, VoicevoxSynthesizer, + helpers::CApiResult, OpenJtalkRc, VoicevoxOnnxruntime, VoicevoxSynthesizer, VoicevoxUserDict, VoicevoxVoiceModelFile, }; @@ -61,10 +70,9 @@ macro_rules! to_cstr { use to_cstr; impl OpenJtalkRc { - pub(crate) fn new(open_jtalk_dic_dir: impl AsRef) -> Result { - Ok(Self { - open_jtalk: voicevox_core::blocking::OpenJtalk::new(open_jtalk_dic_dir)?, - }) + pub(crate) fn new(open_jtalk_dic_dir: impl AsRef) -> Result<&'static Self> { + let body = voicevox_core::blocking::OpenJtalk::new(open_jtalk_dic_dir)?; + Ok(::new(body)) } } @@ -73,42 +81,122 @@ impl VoicevoxSynthesizer { onnxruntime: &'static VoicevoxOnnxruntime, open_jtalk: &OpenJtalkRc, options: &InitializeOptions, - ) -> Result { - let synthesizer = voicevox_core::blocking::Synthesizer::new( + ) -> Result<&'static Self> { + let body = voicevox_core::blocking::Synthesizer::new( &onnxruntime.0, - open_jtalk.open_jtalk.clone(), + (*open_jtalk.body()).clone(), options, )?; - Ok(Self { synthesizer }) + Ok(::new(body)) } pub(crate) fn onnxruntime(&self) -> &'static VoicevoxOnnxruntime { - VoicevoxOnnxruntime::new(self.synthesizer.onnxruntime()) + VoicevoxOnnxruntime::new(self.body().onnxruntime()) } pub(crate) fn load_voice_model( &self, model: &voicevox_core::blocking::VoiceModelFile, ) -> CApiResult<()> { - self.synthesizer.load_voice_model(model)?; + self.body().load_voice_model(model)?; Ok(()) } pub(crate) fn unload_voice_model(&self, model_id: VoiceModelId) -> Result<()> { - self.synthesizer.unload_voice_model(model_id)?; + self.body().unload_voice_model(model_id)?; Ok(()) } pub(crate) fn metas(&self) -> CString { - let metas = &self.synthesizer.metas(); + let metas = &self.body().metas(); CString::new(serde_json::to_string(metas).unwrap()).unwrap() } } impl VoicevoxVoiceModelFile { - pub(crate) fn open(path: impl AsRef) -> Result { + pub(crate) fn open(path: impl AsRef) -> Result<&'static Self> { let model = voicevox_core::blocking::VoiceModelFile::open(path)?; let metas = CString::new(serde_json::to_string(model.metas()).unwrap()).unwrap(); - Ok(Self { model, metas }) + Ok(Self::new(VoiceModelFileWithMetas { model, metas })) + } +} + +pub(crate) struct VoiceModelFileWithMetas { + pub(crate) model: voicevox_core::blocking::VoiceModelFile, + pub(crate) metas: CString, +} + +pub(crate) trait CApiObject: From + Into + Copy + Debug { + type Body: 'static; + + fn heads() -> &'static boxcar::Vec; + fn bodies() -> &'static std::sync::Mutex>>; + + fn new(body: Self::Body) -> &'static Self { + let i = Self::heads().push_with(|i| to_id(i).into()); + Self::lock_bodies().insert(to_id(i), body.into()); + return &Self::heads()[i]; + + fn to_id(i: usize) -> u32 { + i.try_into().expect("too large") + } + } + + /// # Panics + /// + /// 次の場合にパニックする。 + /// + /// * `self`に対して以前にこの関数を呼んでいた場合 + /// * `self`がまだ他で利用中である場合 + fn drop_body(self) { + let body = Self::lock_bodies() + .remove(&self.into()) + .unwrap_or_else(|| self.panic_for_double_delete()); + drop(Arc::into_inner(body).unwrap_or_else(|| { + panic!("`{self:?}`が破棄されようとしましたが、これはまだ利用中です") + })); + } + + /// # Panics + /// + /// `drop_body`を呼んでいるとパニックする。 + fn body(self) -> Arc { + Self::lock_bodies() + .get(&self.into()) + .unwrap_or_else(|| self.panic_for_double_delete()) + .clone() + } +} + +#[ext] +impl T { + fn lock_bodies() -> impl DerefMut>> { + Self::bodies().lock().unwrap_or_else(|e| panic!("{e}")) + } + + fn panic_for_double_delete(self) -> ! { + panic!("`{self:?}`は既に破棄されています"); + } +} + +#[duplicate_item( + H B; + [ OpenJtalkRc ] [ voicevox_core::blocking::OpenJtalk ]; + [ VoicevoxUserDict ] [ voicevox_core::blocking::UserDict ]; + [ VoicevoxSynthesizer ] [ voicevox_core::blocking::Synthesizer ]; + [ VoicevoxVoiceModelFile ] [ VoiceModelFileWithMetas ]; +)] +impl CApiObject for H { + type Body = B; + + fn heads() -> &'static boxcar::Vec { + static HEADS: boxcar::Vec = boxcar::Vec::new(); + &HEADS + } + + fn bodies() -> &'static std::sync::Mutex>> { + static BODIES: LazyLock>>> = + LazyLock::new(Default::default); + &BODIES } } diff --git a/crates/voicevox_core_c_api/src/lib.rs b/crates/voicevox_core_c_api/src/lib.rs index a5c440146..35957ea26 100644 --- a/crates/voicevox_core_c_api/src/lib.rs +++ b/crates/voicevox_core_c_api/src/lib.rs @@ -12,6 +12,7 @@ mod drop_check; mod helpers; mod result_code; mod slice_owner; +use self::c_impls::CApiObject; use self::drop_check::C_STRING_DROP_CHECKER; use self::helpers::{ accent_phrases_to_json, audio_query_model_to_json, ensure_utf8, into_result_code_with_error, @@ -22,7 +23,7 @@ use self::slice_owner::U8_SLICE_OWNER; use anstream::{AutoStream, RawStream}; use chrono::SecondsFormat; use colorchoice::ColorChoice; -use derive_getters::Getters; +use derive_more::{From, Into}; use ref_cast::RefCastCustom; use std::env; use std::ffi::{CStr, CString}; @@ -30,7 +31,7 @@ use std::fmt; use std::io; use std::os::raw::c_char; use std::ptr::NonNull; -use std::sync::{Arc, Once}; +use std::sync::Once; use tracing_subscriber::fmt::format::Writer; use tracing_subscriber::EnvFilter; use uuid::Uuid; @@ -262,8 +263,9 @@ pub unsafe extern "C" fn voicevox_onnxruntime_init_once( /// voicevox_open_jtalk_rc_delete(open_jtalk); /// ``` /// } +#[derive(Clone, Copy, Debug, From, Into)] pub struct OpenJtalkRc { - open_jtalk: voicevox_core::blocking::OpenJtalk, + id: u32, } /// ::OpenJtalkRc を構築(_construct_)する。 @@ -289,7 +291,7 @@ pub struct OpenJtalkRc { #[no_mangle] pub unsafe extern "C" fn voicevox_open_jtalk_rc_new( open_jtalk_dic_dir: *const c_char, - out_open_jtalk: NonNull>, + out_open_jtalk: NonNull>, ) -> VoicevoxResultCode { init_logger_once(); into_result_code_with_error((|| { @@ -318,7 +320,7 @@ pub extern "C" fn voicevox_open_jtalk_rc_use_user_dict( ) -> VoicevoxResultCode { init_logger_once(); into_result_code_with_error((|| { - open_jtalk.open_jtalk.use_user_dict(&user_dict.dict)?; + open_jtalk.body().use_user_dict(&user_dict.body())?; Ok(()) })()) } @@ -338,9 +340,9 @@ pub extern "C" fn voicevox_open_jtalk_rc_use_user_dict( /// - `open_jtalk`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_open_jtalk_rc_delete(open_jtalk: Box) { +pub extern "C" fn voicevox_open_jtalk_rc_delete(open_jtalk: NonNull) { init_logger_once(); - drop(open_jtalk); + unsafe { open_jtalk.as_ref() }.drop_body(); // SAFETY: ユーザーに要求しているもので十分 } /// ハードウェアアクセラレーションモードを設定する設定値。 @@ -397,14 +399,13 @@ pub extern "C" fn voicevox_get_version() -> *const c_char { /// /// VVMファイルと対応する。 /// 構築(_construction_)は ::voicevox_voice_model_file_open で行い、破棄(_destruction_)は ::voicevox_voice_model_file_close で行う。 -#[derive(Getters)] +#[derive(Clone, Copy, Debug, From, Into)] pub struct VoicevoxVoiceModelFile { - model: voicevox_core::blocking::VoiceModelFile, - metas: CString, + id: u32, } /// 音声モデルID。 -pub type VoicevoxVoiceModelId<'a> = &'a [u8; 16]; +pub type VoicevoxVoiceModelId = *const [u8; 16]; /// スタイルID。 /// @@ -425,7 +426,7 @@ pub type VoicevoxStyleId = u32; #[no_mangle] pub unsafe extern "C" fn voicevox_voice_model_file_open( path: *const c_char, - out_model: NonNull>, + out_model: NonNull>, ) -> VoicevoxResultCode { init_logger_once(); into_result_code_with_error((|| { @@ -448,9 +449,9 @@ pub unsafe extern "C" fn voicevox_voice_model_file_open( #[no_mangle] pub extern "C" fn voicevox_voice_model_file_id( model: &VoicevoxVoiceModelFile, -) -> VoicevoxVoiceModelId<'_> { +) -> VoicevoxVoiceModelId { init_logger_once(); - model.model.id_ref().as_bytes() + model.body().model.id_ref().as_bytes() } /// ::VoicevoxVoiceModelFile からメタ情報を取得する。 @@ -468,7 +469,7 @@ pub extern "C" fn voicevox_voice_model_file_get_metas_json( model: &VoicevoxVoiceModelFile, ) -> *const c_char { init_logger_once(); - model.metas().as_ptr() + model.body().metas.as_ptr() } /// ::VoicevoxVoiceModelFile を、所有しているファイルディスクリプタを閉じた上で破棄(_destruct_)する。 @@ -480,17 +481,17 @@ pub extern "C" fn voicevox_voice_model_file_get_metas_json( /// - `model`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_voice_model_file_close(model: Box) { +pub unsafe extern "C" fn voicevox_voice_model_file_close(model: NonNull) { init_logger_once(); - drop(model); + unsafe { model.as_ref() }.drop_body(); // SAFETY: ユーザーに要求しているもので十分 } /// 音声シンセサイザ。 /// /// 構築(_construction_)は ::voicevox_synthesizer_new で行い、破棄(_destruction_)は ::voicevox_synthesizer_delete で行う。 -#[derive(Getters)] +#[derive(Clone, Copy, Debug, From, Into)] pub struct VoicevoxSynthesizer { - synthesizer: voicevox_core::blocking::Synthesizer, + id: u32, } /// ::VoicevoxSynthesizer を構築(_construct_)する。 @@ -512,7 +513,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_new( onnxruntime: &'static VoicevoxOnnxruntime, open_jtalk: &OpenJtalkRc, options: VoicevoxInitializeOptions, - out_synthesizer: NonNull>, + out_synthesizer: NonNull>, ) -> VoicevoxResultCode { init_logger_once(); into_result_code_with_error((|| { @@ -533,9 +534,9 @@ pub unsafe extern "C" fn voicevox_synthesizer_new( /// - `synthesizer`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_synthesizer_delete(synthesizer: Box) { +pub extern "C" fn voicevox_synthesizer_delete(synthesizer: NonNull) { init_logger_once(); - drop(synthesizer); + unsafe { synthesizer.as_ref() }.drop_body(); // SAFETY: ユーザーに要求しているもので十分 } /// 音声モデルを読み込む。 @@ -555,7 +556,7 @@ pub extern "C" fn voicevox_synthesizer_load_voice_model( model: &VoicevoxVoiceModelFile, ) -> VoicevoxResultCode { init_logger_once(); - into_result_code_with_error(synthesizer.load_voice_model(model.model())) + into_result_code_with_error(synthesizer.load_voice_model(&model.body().model)) } /// 音声モデルの読み込みを解除する。 @@ -570,12 +571,12 @@ pub extern "C" fn voicevox_synthesizer_load_voice_model( /// - `model_id`は読み込みについて有効でなければならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_synthesizer_unload_voice_model( +pub unsafe extern "C" fn voicevox_synthesizer_unload_voice_model( synthesizer: &VoicevoxSynthesizer, - model_id: VoicevoxVoiceModelId<'_>, + model_id: VoicevoxVoiceModelId, ) -> VoicevoxResultCode { init_logger_once(); - let model_id = model_id.to_model_id(); + let model_id = unsafe { (*model_id).to_model_id() }; // SAFETY: ユーザーに要求しているもので十分 into_result_code_with_error(synthesizer.unload_voice_model(model_id).map_err(Into::into)) } @@ -607,7 +608,7 @@ pub extern "C" fn voicevox_synthesizer_get_onnxruntime( #[no_mangle] pub extern "C" fn voicevox_synthesizer_is_gpu_mode(synthesizer: &VoicevoxSynthesizer) -> bool { init_logger_once(); - synthesizer.synthesizer().is_gpu_mode() + synthesizer.body().is_gpu_mode() } /// 指定したIDの音声モデルが読み込まれているか判定する。 @@ -622,13 +623,13 @@ pub extern "C" fn voicevox_synthesizer_is_gpu_mode(synthesizer: &VoicevoxSynthes /// - `model_id`は読み込みについて有効でなければならない。 /// } #[no_mangle] -pub extern "C" fn voicevox_synthesizer_is_loaded_voice_model( +pub unsafe extern "C" fn voicevox_synthesizer_is_loaded_voice_model( synthesizer: &VoicevoxSynthesizer, - model_id: VoicevoxVoiceModelId<'_>, + model_id: VoicevoxVoiceModelId, ) -> bool { init_logger_once(); - let model_id = model_id.to_model_id(); - synthesizer.synthesizer().is_loaded_voice_model(model_id) + let model_id = unsafe { (*model_id).to_model_id() }; // SAFETY: ユーザーに要求しているもので十分 + synthesizer.body().is_loaded_voice_model(model_id) } /// 今読み込んでいる音声モデルのメタ情報を、JSONで取得する。 @@ -729,7 +730,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_audio_query_from_kana( let kana = ensure_utf8(kana)?; let audio_query = synthesizer - .synthesizer() + .body() .audio_query_from_kana(kana, StyleId::new(style_id))?; let audio_query = CString::new(audio_query_model_to_json(&audio_query)) .expect("should not contain '\\0'"); @@ -777,7 +778,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_audio_query( let text = ensure_utf8(text)?; let audio_query = synthesizer - .synthesizer() + .body() .audio_query(text, StyleId::new(style_id))?; let audio_query = CString::new(audio_query_model_to_json(&audio_query)) .expect("should not contain '\\0'"); @@ -824,7 +825,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_accent_phrases_from_kana( into_result_code_with_error((|| { let kana = ensure_utf8(CStr::from_ptr(kana))?; let accent_phrases = synthesizer - .synthesizer() + .body() .create_accent_phrases_from_kana(kana, StyleId::new(style_id))?; let accent_phrases = CString::new(accent_phrases_to_json(&accent_phrases)) .expect("should not contain '\\0'"); @@ -870,7 +871,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_accent_phrases( into_result_code_with_error((|| { let text = ensure_utf8(CStr::from_ptr(text))?; let accent_phrases = synthesizer - .synthesizer() + .body() .create_accent_phrases(text, StyleId::new(style_id))?; let accent_phrases = CString::new(accent_phrases_to_json(&accent_phrases)) .expect("should not contain '\\0'"); @@ -909,7 +910,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_replace_mora_data( serde_json::from_str(ensure_utf8(CStr::from_ptr(accent_phrases_json))?) .map_err(CApiError::InvalidAccentPhrase)?; let accent_phrases = synthesizer - .synthesizer() + .body() .replace_mora_data(&accent_phrases, StyleId::new(style_id))?; let accent_phrases = CString::new(accent_phrases_to_json(&accent_phrases)) .expect("should not contain '\\0'"); @@ -948,7 +949,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_replace_phoneme_length( serde_json::from_str(ensure_utf8(CStr::from_ptr(accent_phrases_json))?) .map_err(CApiError::InvalidAccentPhrase)?; let accent_phrases = synthesizer - .synthesizer() + .body() .replace_phoneme_length(&accent_phrases, StyleId::new(style_id))?; let accent_phrases = CString::new(accent_phrases_to_json(&accent_phrases)) .expect("should not contain '\\0'"); @@ -987,7 +988,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_replace_mora_pitch( serde_json::from_str(ensure_utf8(CStr::from_ptr(accent_phrases_json))?) .map_err(CApiError::InvalidAccentPhrase)?; let accent_phrases = synthesizer - .synthesizer() + .body() .replace_mora_pitch(&accent_phrases, StyleId::new(style_id))?; let accent_phrases = CString::new(accent_phrases_to_json(&accent_phrases)) .expect("should not contain '\\0'"); @@ -1047,7 +1048,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_synthesis( .map_err(|_| CApiError::InvalidUtf8Input)?; let audio_query: AudioQuery = serde_json::from_str(audio_query_json).map_err(CApiError::InvalidAudioQuery)?; - let wav = synthesizer.synthesizer().synthesis( + let wav = synthesizer.body().synthesis( &audio_query, StyleId::new(style_id), &SynthesisOptions::from(options), @@ -1103,7 +1104,7 @@ pub unsafe extern "C" fn voicevox_synthesizer_tts_from_kana( init_logger_once(); into_result_code_with_error((|| { let kana = ensure_utf8(CStr::from_ptr(kana))?; - let output = synthesizer.synthesizer().tts_from_kana( + let output = synthesizer.body().tts_from_kana( kana, StyleId::new(style_id), &TtsOptions::from(options), @@ -1144,11 +1145,10 @@ pub unsafe extern "C" fn voicevox_synthesizer_tts( init_logger_once(); into_result_code_with_error((|| { let text = ensure_utf8(CStr::from_ptr(text))?; - let output = synthesizer.synthesizer().tts( - text, - StyleId::new(style_id), - &TtsOptions::from(options), - )?; + let output = + synthesizer + .body() + .tts(text, StyleId::new(style_id), &TtsOptions::from(options))?; U8_SLICE_OWNER.own_and_lend(output, output_wav, output_wav_length); Ok(()) })()) @@ -1225,9 +1225,9 @@ pub extern "C" fn voicevox_error_result_to_message( } /// ユーザー辞書。 -#[derive(Default)] +#[derive(Clone, Copy, Debug, From, Into)] pub struct VoicevoxUserDict { - dict: Arc, + id: u32, } /// ユーザー辞書の単語。 @@ -1290,9 +1290,9 @@ pub extern "C" fn voicevox_user_dict_word_make( /// /// @returns ::VoicevoxUserDict #[no_mangle] -pub extern "C" fn voicevox_user_dict_new() -> Box { +pub extern "C" fn voicevox_user_dict_new() -> NonNull { init_logger_once(); - Default::default() + VoicevoxUserDict::new(Default::default()).into() } /// ユーザー辞書にファイルを読み込ませる。 @@ -1313,7 +1313,7 @@ pub unsafe extern "C" fn voicevox_user_dict_load( init_logger_once(); into_result_code_with_error((|| { let dict_path = ensure_utf8(unsafe { CStr::from_ptr(dict_path) })?; - user_dict.dict.load(dict_path)?; + user_dict.body().load(dict_path)?; Ok(()) })()) @@ -1343,7 +1343,7 @@ pub unsafe extern "C" fn voicevox_user_dict_add_word( init_logger_once(); into_result_code_with_error((|| { let word = word.read_unaligned().try_into_word()?; - let uuid = user_dict.dict.add_word(word)?; + let uuid = user_dict.body().add_word(word)?; output_word_uuid.write_unaligned(uuid.into_bytes()); Ok(()) @@ -1372,7 +1372,7 @@ pub unsafe extern "C" fn voicevox_user_dict_update_word( into_result_code_with_error((|| { let word_uuid = Uuid::from_slice(word_uuid).map_err(CApiError::InvalidUuid)?; let word = word.read_unaligned().try_into_word()?; - user_dict.dict.update_word(word_uuid, word)?; + user_dict.body().update_word(word_uuid, word)?; Ok(()) })()) @@ -1396,7 +1396,7 @@ pub extern "C" fn voicevox_user_dict_remove_word( init_logger_once(); into_result_code_with_error((|| { let word_uuid = Uuid::from_slice(word_uuid).map_err(CApiError::InvalidUuid)?; - user_dict.dict.remove_word(word_uuid)?; + user_dict.body().remove_word(word_uuid)?; Ok(()) })()) } @@ -1420,7 +1420,7 @@ pub unsafe extern "C" fn voicevox_user_dict_to_json( output_json: NonNull<*mut c_char>, ) -> VoicevoxResultCode { init_logger_once(); - let json = user_dict.dict.to_json(); + let json = user_dict.body().to_json(); let json = CString::new(json).expect("\\0を含まない文字列であることが保証されている"); output_json.write_unaligned(C_STRING_DROP_CHECKER.whitelist(json).into_raw()); VoicevoxResultCode::VOICEVOX_RESULT_OK @@ -1442,7 +1442,7 @@ pub extern "C" fn voicevox_user_dict_import( ) -> VoicevoxResultCode { init_logger_once(); into_result_code_with_error((|| { - user_dict.dict.import(&other_dict.dict)?; + user_dict.body().import(&other_dict.body())?; Ok(()) })()) } @@ -1464,7 +1464,7 @@ pub unsafe extern "C" fn voicevox_user_dict_save( init_logger_once(); into_result_code_with_error((|| { let path = ensure_utf8(CStr::from_ptr(path))?; - user_dict.dict.save(path)?; + user_dict.body().save(path)?; Ok(()) })()) } @@ -1477,7 +1477,7 @@ pub unsafe extern "C" fn voicevox_user_dict_save( /// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。 /// } #[no_mangle] -pub unsafe extern "C" fn voicevox_user_dict_delete(user_dict: Box) { +pub unsafe extern "C" fn voicevox_user_dict_delete(user_dict: NonNull) { init_logger_once(); - drop(user_dict); + unsafe { user_dict.as_ref() }.drop_body(); // SAFETY: ユーザーに要求しているもので十分 } diff --git a/crates/voicevox_core_c_api/tests/e2e/snapshots.toml b/crates/voicevox_core_c_api/tests/e2e/snapshots.toml index eb5911c7e..cdb2dbc29 100644 --- a/crates/voicevox_core_c_api/tests/e2e/snapshots.toml +++ b/crates/voicevox_core_c_api/tests/e2e/snapshots.toml @@ -64,6 +64,30 @@ stderr.unix = ''' last_error_message = "Statusが初期化されていません" stderr = "" +[double_delete_openjtalk] +stderr_contains_all = [ + "\n`OpenJtalkRc { id: 0 }`は既に破棄されています\n", + "\nthread caused non-unwinding panic. aborting.\n", +] + +[double_delete_synthesizer] +stderr_contains_all = [ + "\n`VoicevoxSynthesizer { id: 0 }`は既に破棄されています\n", + "\nthread caused non-unwinding panic. aborting.\n", +] + +[double_delete_user_dict] +stderr_contains_all = [ + "\n`VoicevoxUserDict { id: 0 }`は既に破棄されています\n", + "\nthread caused non-unwinding panic. aborting.\n", +] + +[double_delete_voice_model_file] +stderr_contains_all = [ + "\n`VoicevoxVoiceModelFile { id: 0 }`は既に破棄されています\n", + "\nthread caused non-unwinding panic. aborting.\n", +] + [global_info] result_messages.0 = "エラーが発生しませんでした" result_messages.1 = "OpenJTalkの辞書が読み込まれていません" diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases.rs b/crates/voicevox_core_c_api/tests/e2e/testcases.rs index 31eb9cdfe..677d2855f 100644 --- a/crates/voicevox_core_c_api/tests/e2e/testcases.rs +++ b/crates/voicevox_core_c_api/tests/e2e/testcases.rs @@ -1,5 +1,9 @@ mod compatible_engine; mod compatible_engine_load_model_before_initialize; +mod double_delete_openjtalk; +mod double_delete_synthesizer; +mod double_delete_user_dict; +mod double_delete_voice_model_file; mod global_info; mod simple_tts; mod synthesizer_new_output_json; diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_openjtalk.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_openjtalk.rs new file mode 100644 index 000000000..20efff861 --- /dev/null +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_openjtalk.rs @@ -0,0 +1,61 @@ +//! `voicevox_open_jtalk_rc_delete`を二度呼ぶとクラッシュすることを確認する。 + +use std::{ffi::CString, mem::MaybeUninit, sync::LazyLock}; + +use assert_cmd::assert::AssertResult; +use indexmap::IndexSet; +use libloading::Library; +use serde::{Deserialize, Serialize}; +use test_util::{ + c_api::{self, CApi, VoicevoxResultCode}, + OPEN_JTALK_DIC_DIR, +}; + +use crate::{ + assert_cdylib::{self, case, Utf8Output}, + snapshots, +}; + +case!(TestCase); + +#[derive(Serialize, Deserialize)] +struct TestCase; + +#[typetag::serde(name = "double_delete_openjtalk")] +impl assert_cdylib::TestCase for TestCase { + unsafe fn exec(&self, lib: Library) -> anyhow::Result<()> { + let lib = CApi::from_library(lib)?; + + let openjtalk = { + let mut openjtalk = MaybeUninit::uninit(); + let open_jtalk_dic_dir = CString::new(OPEN_JTALK_DIC_DIR).unwrap(); + assert_ok( + lib.voicevox_open_jtalk_rc_new(open_jtalk_dic_dir.as_ptr(), openjtalk.as_mut_ptr()), + ); + openjtalk.assume_init() + }; + + lib.voicevox_open_jtalk_rc_delete(openjtalk); + lib.voicevox_open_jtalk_rc_delete(openjtalk); + unreachable!(); + + fn assert_ok(result_code: VoicevoxResultCode) { + std::assert_eq!(c_api::VoicevoxResultCode_VOICEVOX_RESULT_OK, result_code); + } + } + + fn assert_output(&self, output: Utf8Output) -> AssertResult { + let mut assert = output.assert().try_failure()?.try_stdout("")?; + for s in &SNAPSHOTS.stderr_contains_all { + assert = assert.try_stderr(predicates::str::contains(s))?; + } + Ok(assert) + } +} + +static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_openjtalk); + +#[derive(Deserialize)] +struct Snapshots { + stderr_contains_all: IndexSet, +} diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_synthesizer.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_synthesizer.rs new file mode 100644 index 000000000..22fdb92c9 --- /dev/null +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_synthesizer.rs @@ -0,0 +1,85 @@ +//! `voicevox_synthesizer_delete`を二度呼ぶとクラッシュすることを確認する。 + +use std::{ffi::CString, mem::MaybeUninit, sync::LazyLock}; + +use assert_cmd::assert::AssertResult; +use indexmap::IndexSet; +use libloading::Library; +use serde::{Deserialize, Serialize}; +use test_util::{ + c_api::{self, CApi, VoicevoxInitializeOptions, VoicevoxResultCode}, + OPEN_JTALK_DIC_DIR, +}; + +use crate::{ + assert_cdylib::{self, case, Utf8Output}, + snapshots, +}; + +case!(TestCase); + +#[derive(Serialize, Deserialize)] +struct TestCase; + +#[typetag::serde(name = "double_delete_synthesizer")] +impl assert_cdylib::TestCase for TestCase { + unsafe fn exec(&self, lib: Library) -> anyhow::Result<()> { + let lib = CApi::from_library(lib)?; + + let onnxruntime = { + let mut onnxruntime = MaybeUninit::uninit(); + assert_ok(lib.voicevox_onnxruntime_load_once( + lib.voicevox_make_default_load_onnxruntime_options(), + onnxruntime.as_mut_ptr(), + )); + onnxruntime.assume_init() + }; + + let openjtalk = { + let mut openjtalk = MaybeUninit::uninit(); + let open_jtalk_dic_dir = CString::new(OPEN_JTALK_DIC_DIR).unwrap(); + assert_ok( + lib.voicevox_open_jtalk_rc_new(open_jtalk_dic_dir.as_ptr(), openjtalk.as_mut_ptr()), + ); + openjtalk.assume_init() + }; + + let synthesizer = { + let mut synthesizer = MaybeUninit::uninit(); + assert_ok(lib.voicevox_synthesizer_new( + onnxruntime, + openjtalk, + VoicevoxInitializeOptions { + acceleration_mode: + c_api::VoicevoxAccelerationMode_VOICEVOX_ACCELERATION_MODE_CPU, + ..lib.voicevox_make_default_initialize_options() + }, + synthesizer.as_mut_ptr(), + )); + synthesizer.assume_init() + }; + + lib.voicevox_synthesizer_delete(synthesizer); + lib.voicevox_synthesizer_delete(synthesizer); + unreachable!(); + + fn assert_ok(result_code: VoicevoxResultCode) { + std::assert_eq!(c_api::VoicevoxResultCode_VOICEVOX_RESULT_OK, result_code); + } + } + + fn assert_output(&self, output: Utf8Output) -> AssertResult { + let mut assert = output.assert().try_failure()?.try_stdout("")?; + for s in &SNAPSHOTS.stderr_contains_all { + assert = assert.try_stderr(predicates::str::contains(s))?; + } + Ok(assert) + } +} + +static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_synthesizer); + +#[derive(Deserialize)] +struct Snapshots { + stderr_contains_all: IndexSet, +} diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_user_dict.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_user_dict.rs new file mode 100644 index 000000000..13436e8fa --- /dev/null +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_user_dict.rs @@ -0,0 +1,46 @@ +//! `voicevox_user_dict_delete`を二度呼ぶとクラッシュすることを確認する。 + +use std::sync::LazyLock; + +use assert_cmd::assert::AssertResult; +use indexmap::IndexSet; +use libloading::Library; +use serde::{Deserialize, Serialize}; +use test_util::c_api::CApi; + +use crate::{ + assert_cdylib::{self, case, Utf8Output}, + snapshots, +}; + +case!(TestCase); + +#[derive(Serialize, Deserialize)] +struct TestCase; + +#[typetag::serde(name = "double_delete_user_dict")] +impl assert_cdylib::TestCase for TestCase { + unsafe fn exec(&self, lib: Library) -> anyhow::Result<()> { + let lib = CApi::from_library(lib)?; + + let dict = lib.voicevox_user_dict_new(); + lib.voicevox_user_dict_delete(dict); + lib.voicevox_user_dict_delete(dict); + unreachable!(); + } + + fn assert_output(&self, output: Utf8Output) -> AssertResult { + let mut assert = output.assert().try_failure()?.try_stdout("")?; + for s in &SNAPSHOTS.stderr_contains_all { + assert = assert.try_stderr(predicates::str::contains(s))?; + } + Ok(assert) + } +} + +static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_user_dict); + +#[derive(Deserialize)] +struct Snapshots { + stderr_contains_all: IndexSet, +} diff --git a/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_voice_model_file.rs b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_voice_model_file.rs new file mode 100644 index 000000000..573f6bb1b --- /dev/null +++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_voice_model_file.rs @@ -0,0 +1,58 @@ +//! `voicevox_voice_model_file_close`を二度呼ぶとクラッシュすることを確認する。 + +use std::{mem::MaybeUninit, sync::LazyLock}; + +use assert_cmd::assert::AssertResult; +use indexmap::IndexSet; +use libloading::Library; +use serde::{Deserialize, Serialize}; +use test_util::c_api::{self, CApi, VoicevoxResultCode}; + +use crate::{ + assert_cdylib::{self, case, Utf8Output}, + snapshots, +}; + +case!(TestCase); + +#[derive(Serialize, Deserialize)] +struct TestCase; + +#[typetag::serde(name = "double_delete_voice_model_file")] +impl assert_cdylib::TestCase for TestCase { + unsafe fn exec(&self, lib: Library) -> anyhow::Result<()> { + let lib = CApi::from_library(lib)?; + + let model = { + let mut model = MaybeUninit::uninit(); + assert_ok(lib.voicevox_voice_model_file_open( + c_api::SAMPLE_VOICE_MODEL_FILE_PATH.as_ptr(), + model.as_mut_ptr(), + )); + model.assume_init() + }; + + lib.voicevox_voice_model_file_close(model); + lib.voicevox_voice_model_file_close(model); + unreachable!(); + + fn assert_ok(result_code: VoicevoxResultCode) { + std::assert_eq!(c_api::VoicevoxResultCode_VOICEVOX_RESULT_OK, result_code); + } + } + + fn assert_output(&self, output: Utf8Output) -> AssertResult { + let mut assert = output.assert().try_failure()?.try_stdout("")?; + for s in &SNAPSHOTS.stderr_contains_all { + assert = assert.try_stderr(predicates::str::contains(s))?; + } + Ok(assert) + } +} + +static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_voice_model_file); + +#[derive(Deserialize)] +struct Snapshots { + stderr_contains_all: IndexSet, +}