diff --git a/Cargo.lock b/Cargo.lock
index 2c2be4f8d..1dd3d5d53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1369,6 +1369,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"
@@ -2226,6 +2235,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 +2762,10 @@ checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97"
dependencies = [
"anstyle",
"difflib",
+ "float-cmp",
+ "normalize-line-endings",
"predicates-core",
+ "regex",
]
[[package]]
@@ -4362,9 +4380,11 @@ dependencies = [
"colorchoice",
"const_format",
"cstr",
- "derive-getters",
"duct",
+ "duplicate",
"easy-ext",
+ "educe",
+ "indexmap 2.6.0",
"inventory",
"itertools 0.10.5",
"libc",
@@ -4372,6 +4392,8 @@ dependencies = [
"libtest-mimic",
"ndarray",
"ndarray-stats",
+ "parking_lot",
+ "predicates",
"process_path",
"ref-cast",
"regex",
diff --git a/Cargo.toml b/Cargo.toml
index 627fab02f..69ecbb58a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -58,8 +58,10 @@ ndarray-stats = "0.5.1"
octocrab = { version = "0.19.0", default-features = false }
once_cell = "1.20.1"
ouroboros = "0.18.4"
+parking_lot = "0.12.1"
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..7c4484907 100644
--- a/crates/voicevox_core_c_api/Cargo.toml
+++ b/crates/voicevox_core_c_api/Cargo.toml
@@ -25,10 +25,12 @@ chrono = { workspace = true, default-features = false, features = ["clock"] }
colorchoice.workspace = true
const_format.workspace = true
cstr.workspace = true
-derive-getters.workspace = true
+duplicate.workspace = true
easy-ext.workspace = true
+educe.workspace = true
itertools.workspace = true
libc.workspace = true
+parking_lot = { workspace = true, features = ["arc_lock"] }
process_path.workspace = true
ref-cast.workspace = true
serde_json = { workspace = true, features = ["preserve_order"] }
@@ -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/include/voicevox_core.h b/crates/voicevox_core_c_api/include/voicevox_core.h
index e8656d8ec..9fc461000 100644
--- a/crates/voicevox_core_c_api/include/voicevox_core.h
+++ b/crates/voicevox_core_c_api/include/voicevox_core.h
@@ -541,11 +541,6 @@ VoicevoxResultCode voicevox_open_jtalk_rc_new(const char *open_jtalk_dic_dir,
*
* @param [in] open_jtalk Open JTalkのオブジェクト
* @param [in] user_dict ユーザー辞書
- *
- * \safety{
- * - `open_jtalk`は ::voicevox_open_jtalk_rc_new で得たものでなければならず、また ::voicevox_open_jtalk_rc_delete で解放されていてはいけない。
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -556,6 +551,10 @@ VoicevoxResultCode voicevox_open_jtalk_rc_use_user_dict(const struct OpenJtalkRc
/**
* ::OpenJtalkRc を破棄(_destruct_)する。
*
+ * 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
+ *
+ * この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+ *
* @param [in] open_jtalk 破棄対象
*
* \example{
@@ -563,11 +562,6 @@ VoicevoxResultCode voicevox_open_jtalk_rc_use_user_dict(const struct OpenJtalkRc
* voicevox_open_jtalk_rc_delete(open_jtalk);
* ```
* }
- *
- * \safety{
- * - `open_jtalk`は ::voicevox_open_jtalk_rc_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。
- * - `open_jtalk`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -618,7 +612,6 @@ VoicevoxResultCode voicevox_voice_model_file_open(const char *path,
* @param [out] output_voice_model_id 音声モデルID
*
* \safety{
- * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。
* - `output_voice_model_id`は書き込みについて有効でなければならない。
* }
*/
@@ -636,10 +629,6 @@ void voicevox_voice_model_file_id(const struct VoicevoxVoiceModelFile *model,
* @param [in] model 音声モデル
*
* @returns メタ情報のJSON文字列
- *
- * \safety{
- * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -649,12 +638,11 @@ char *voicevox_voice_model_file_create_metas_json(const struct VoicevoxVoiceMode
/**
* ::VoicevoxVoiceModelFile を、所有しているファイルディスクリプタを閉じた上で破棄(_destruct_)する。
*
- * @param [in] model 破棄対象
+ * 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
*
- * \safety{
- * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また既にこの関数で解放されていてはいけない。
- * - `model`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。
- * }
+ * この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+ *
+ * @param [in] model 破棄対象
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -673,7 +661,6 @@ void voicevox_voice_model_file_close(struct VoicevoxVoiceModelFile *model);
*
* \safety{
* - `onnxruntime`は ::voicevox_onnxruntime_load_once または ::voicevox_onnxruntime_init_once で得たものでなければならない。
- * - `open_jtalk`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_open_jtalk_rc_new で解放されていてはいけない。
* - `out_synthesizer`は書き込みについて有効でなければならない。
* }
*/
@@ -688,12 +675,11 @@ VoicevoxResultCode voicevox_synthesizer_new(const struct VoicevoxOnnxruntime *on
/**
* ::VoicevoxSynthesizer を破棄(_destruct_)する。
*
- * @param [in] synthesizer 破棄対象
+ * 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
*
- * \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。
- * - `synthesizer`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。
- * }
+ * この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+ *
+ * @param [in] synthesizer 破棄対象
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -707,11 +693,6 @@ void voicevox_synthesizer_delete(struct VoicevoxSynthesizer *synthesizer);
* @param [in] model 音声モデル
*
* @returns 結果コード
- *
- * \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
- * - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -728,7 +709,6 @@ VoicevoxResultCode voicevox_synthesizer_load_voice_model(const struct VoicevoxSy
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `model_id`は読み込みについて有効でなければならない。
* }
*/
@@ -744,10 +724,6 @@ VoicevoxResultCode voicevox_synthesizer_unload_voice_model(const struct Voicevox
* @param [in] synthesizer 音声シンセサイザ
*
* @returns ::VoicevoxOnnxruntime のインスタンス
- *
- * \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -760,10 +736,6 @@ const struct VoicevoxOnnxruntime *voicevox_synthesizer_get_onnxruntime(const str
* @param [in] synthesizer 音声シンセサイザ
*
* @returns GPUモードかどうか
- *
- * \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -779,7 +751,6 @@ bool voicevox_synthesizer_is_gpu_mode(const struct VoicevoxSynthesizer *synthesi
* @returns モデルが読み込まれているかどうか
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `model_id`は読み込みについて有効でなければならない。
* }
*/
@@ -797,10 +768,6 @@ bool voicevox_synthesizer_is_loaded_voice_model(const struct VoicevoxSynthesizer
* @param [in] synthesizer 音声シンセサイザ
*
* @return メタ情報のJSON文字列
- *
- * \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -859,7 +826,6 @@ VoicevoxResultCode voicevox_onnxruntime_create_supported_devices_json(const stru
* }
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `kana`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -894,7 +860,6 @@ VoicevoxResultCode voicevox_synthesizer_create_audio_query_from_kana(const struc
* }
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `text`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -930,7 +895,6 @@ VoicevoxResultCode voicevox_synthesizer_create_audio_query(const struct Voicevox
* }
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `kana`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -965,7 +929,6 @@ VoicevoxResultCode voicevox_synthesizer_create_accent_phrases_from_kana(const st
* }
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `text`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -991,7 +954,6 @@ VoicevoxResultCode voicevox_synthesizer_create_accent_phrases(const struct Voice
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `accent_phrases_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -1017,7 +979,6 @@ VoicevoxResultCode voicevox_synthesizer_replace_mora_data(const struct VoicevoxS
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `accent_phrases_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -1043,7 +1004,6 @@ VoicevoxResultCode voicevox_synthesizer_replace_phoneme_length(const struct Voic
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `accent_phrases_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_audio_query_json`は書き込みについて有効でなければならない。
* }
@@ -1080,7 +1040,6 @@ struct VoicevoxSynthesisOptions voicevox_make_default_synthesis_options(void);
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `audio_query_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_wav_length`は書き込みについて有効でなければならない。
* - `output_wav`は書き込みについて有効でなければならない。
@@ -1120,7 +1079,6 @@ struct VoicevoxTtsOptions voicevox_make_default_tts_options(void);
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `kana`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_wav_length`は書き込みについて有効でなければならない。
* - `output_wav`は書き込みについて有効でなければならない。
@@ -1151,7 +1109,6 @@ VoicevoxResultCode voicevox_synthesizer_tts_from_kana(const struct VoicevoxSynth
* @returns 結果コード
*
* \safety{
- * - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
* - `text`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_wav_length`は書き込みについて有効でなければならない。
* - `output_wav`は書き込みについて有効でなければならない。
@@ -1269,7 +1226,6 @@ struct VoicevoxUserDict *voicevox_user_dict_new(void);
* @returns 結果コード
*
* \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
* - `dict_path`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* }
*/
@@ -1291,7 +1247,6 @@ VoicevoxResultCode voicevox_user_dict_load(const struct VoicevoxUserDict *user_d
* @param user_dict は有効な :VoicevoxUserDict のポインタであること
*
* \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
* - `word->surface`と`word->pronunciation`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* - `output_word_uuid`は書き込みについて有効でなければならない。
* }
@@ -1312,7 +1267,6 @@ VoicevoxResultCode voicevox_user_dict_add_word(const struct VoicevoxUserDict *us
* @returns 結果コード
*
* \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
* - `word_uuid`は読み込みについて有効でなければならない。
* - `word->surface`と`word->pronunciation`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* }
@@ -1332,7 +1286,6 @@ VoicevoxResultCode voicevox_user_dict_update_word(const struct VoicevoxUserDict
* @returns 結果コード
*
* \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
* - `word_uuid`は読み込みについて有効でなければならない。
* }
*/
@@ -1352,7 +1305,6 @@ VoicevoxResultCode voicevox_user_dict_remove_word(const struct VoicevoxUserDict
* @returns 結果コード
*
* \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
* - `output_json`は書き込みについて有効でなければならない。
* }
*/
@@ -1368,10 +1320,6 @@ VoicevoxResultCode voicevox_user_dict_to_json(const struct VoicevoxUserDict *use
* @param [in] user_dict ユーザー辞書
* @param [in] other_dict インポートするユーザー辞書
* @returns 結果コード
- *
- * \safety{
- * - `user_dict`と`other_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
- * }
*/
#ifdef _WIN32
__declspec(dllimport)
@@ -1386,7 +1334,6 @@ VoicevoxResultCode voicevox_user_dict_import(const struct VoicevoxUserDict *user
* @param [in] path 保存先のファイルパス
*
* \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
* - `path`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
* }
*/
@@ -1399,11 +1346,11 @@ VoicevoxResultCode voicevox_user_dict_save(const struct VoicevoxUserDict *user_d
/**
* ユーザー辞書を破棄(_destruct_)する。
*
- * @param [in] user_dict 破棄対象
+ * 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
*
- * \safety{
- * - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。
- * }
+ * この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+ *
+ * @param [in] user_dict 破棄対象
*/
#ifdef _WIN32
__declspec(dllimport)
diff --git a/crates/voicevox_core_c_api/src/c_impls.rs b/crates/voicevox_core_c_api/src/c_impls.rs
index 8888cfdfe..c657d0908 100644
--- a/crates/voicevox_core_c_api/src/c_impls.rs
+++ b/crates/voicevox_core_c_api/src/c_impls.rs
@@ -1,11 +1,21 @@
-use std::{ffi::CString, path::Path};
+use std::{
+ collections::HashMap,
+ ffi::CString,
+ path::Path,
+ ptr::NonNull,
+ 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, SpeakerMeta, VoiceModelId};
use crate::{
- helpers::CApiResult, OpenJtalkRc, VoicevoxOnnxruntime, VoicevoxSynthesizer,
+ helpers::CApiResult,
+ object::{CApiObject, CApiObjectPtrExt as _},
+ OpenJtalkRc, VoicevoxOnnxruntime, VoicevoxSynthesizer, VoicevoxUserDict,
VoicevoxVoiceModelFile,
};
@@ -61,57 +71,62 @@ 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> {
+ let body = voicevox_core::blocking::OpenJtalk::new(open_jtalk_dic_dir)?;
+ Ok(::new(body))
}
}
impl VoicevoxSynthesizer {
pub(crate) fn new(
onnxruntime: &'static VoicevoxOnnxruntime,
- open_jtalk: &OpenJtalkRc,
+ open_jtalk: *const OpenJtalkRc,
options: &InitializeOptions,
- ) -> Result {
- let synthesizer = voicevox_core::blocking::Synthesizer::new(
+ ) -> Result> {
+ 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())
+#[ext(VoicevoxSynthesizerPtrExt)]
+impl *const VoicevoxSynthesizer {
+ pub(crate) fn onnxruntime(self) -> &'static VoicevoxOnnxruntime {
+ VoicevoxOnnxruntime::new(self.body().onnxruntime())
}
pub(crate) fn load_voice_model(
- &self,
+ 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)?;
+ pub(crate) fn unload_voice_model(self, model_id: VoiceModelId) -> Result<()> {
+ self.body().unload_voice_model(model_id)?;
Ok(())
}
- pub(crate) fn metas(&self) -> CString {
- metas_to_json(&self.synthesizer.metas())
+ pub(crate) fn metas(self) -> CString {
+ metas_to_json(&self.body().metas())
}
}
impl VoicevoxVoiceModelFile {
- pub(crate) fn open(path: impl AsRef) -> Result {
+ pub(crate) fn open(path: impl AsRef) -> Result> {
let model = voicevox_core::blocking::VoiceModelFile::open(path)?;
- Ok(Self { model })
+ Ok(Self::new(model))
}
+}
- pub(crate) fn metas(&self) -> CString {
- metas_to_json(self.model.metas())
+#[ext(VoicevoxVoiceModelFilePtrExt)]
+impl *const VoicevoxVoiceModelFile {
+ pub(crate) fn metas(self) -> CString {
+ metas_to_json(self.body().metas())
}
}
@@ -119,3 +134,30 @@ fn metas_to_json(metas: &[SpeakerMeta]) -> CString {
let metas = serde_json::to_string(metas).expect("should not fail");
CString::new(metas).expect("should not contain NUL")
}
+
+#[duplicate_item(
+ H B;
+ [ OpenJtalkRc ] [ voicevox_core::blocking::OpenJtalk ];
+ [ VoicevoxUserDict ] [ voicevox_core::blocking::UserDict ];
+ [ VoicevoxSynthesizer ] [ voicevox_core::blocking::Synthesizer ];
+ [ VoicevoxVoiceModelFile ] [ voicevox_core::blocking::VoiceModelFile ];
+)]
+impl CApiObject for H {
+ type RustApiObject = B;
+
+ fn heads() -> &'static std::sync::Mutex> {
+ static HEADS: std::sync::Mutex> = std::sync::Mutex::new(vec![]);
+ &HEADS
+ }
+
+ fn bodies() -> &'static std::sync::Mutex<
+ HashMap>>>,
+ > {
+ #[expect(clippy::type_complexity, reason = "`CApiObject::bodies`と同様")]
+ static BODIES: LazyLock<
+ std::sync::Mutex>>>>,
+ > = 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 a43ea03cf..76a23a442 100644
--- a/crates/voicevox_core_c_api/src/lib.rs
+++ b/crates/voicevox_core_c_api/src/lib.rs
@@ -10,6 +10,7 @@ mod c_impls;
mod compatible_engine;
mod drop_check;
mod helpers;
+mod object;
mod result_code;
mod slice_owner;
use self::drop_check::C_STRING_DROP_CHECKER;
@@ -17,20 +18,23 @@ use self::helpers::{
accent_phrases_to_json, audio_query_model_to_json, ensure_utf8, into_result_code_with_error,
CApiError, UuidBytesExt as _,
};
+use self::object::{CApiObject as _, CApiObjectPtrExt as _};
use self::result_code::VoicevoxResultCode;
use self::slice_owner::U8_SLICE_OWNER;
use anstream::{AutoStream, RawStream};
+use c_impls::{VoicevoxSynthesizerPtrExt as _, VoicevoxVoiceModelFilePtrExt as _};
use chrono::SecondsFormat;
use colorchoice::ColorChoice;
-use derive_getters::Getters;
+use educe::Educe;
use ref_cast::RefCastCustom;
use std::env;
use std::ffi::{CStr, CString};
use std::fmt;
use std::io;
+use std::mem::MaybeUninit;
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;
@@ -261,8 +265,10 @@ pub unsafe extern "C" fn voicevox_onnxruntime_init_once(
/// voicevox_open_jtalk_rc_delete(open_jtalk);
/// ```
/// }
+#[derive(Debug, Educe)]
+#[educe(Default(expression = "Self { _padding: MaybeUninit::uninit() }"))]
pub struct OpenJtalkRc {
- open_jtalk: voicevox_core::blocking::OpenJtalk,
+ _padding: MaybeUninit<[u8; 1]>,
}
/// ::OpenJtalkRc を構築(_construct_)する。
@@ -288,12 +294,12 @@ 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((|| {
let open_jtalk_dic_dir = ensure_utf8(CStr::from_ptr(open_jtalk_dic_dir))?;
- let open_jtalk = OpenJtalkRc::new(open_jtalk_dic_dir)?.into();
+ let open_jtalk = OpenJtalkRc::new(open_jtalk_dic_dir)?;
out_open_jtalk.write_unaligned(open_jtalk);
Ok(())
})())
@@ -305,25 +311,24 @@ pub unsafe extern "C" fn voicevox_open_jtalk_rc_new(
///
/// @param [in] open_jtalk Open JTalkのオブジェクト
/// @param [in] user_dict ユーザー辞書
-///
-/// \safety{
-/// - `open_jtalk`は ::voicevox_open_jtalk_rc_new で得たものでなければならず、また ::voicevox_open_jtalk_rc_delete で解放されていてはいけない。
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
-/// }
#[no_mangle]
pub extern "C" fn voicevox_open_jtalk_rc_use_user_dict(
- open_jtalk: &OpenJtalkRc,
- user_dict: &VoicevoxUserDict,
+ open_jtalk: *const OpenJtalkRc,
+ user_dict: *const VoicevoxUserDict,
) -> 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(())
})())
}
/// ::OpenJtalkRc を破棄(_destruct_)する。
///
+/// 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
+///
+/// この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+///
/// @param [in] open_jtalk 破棄対象
///
/// \example{
@@ -331,15 +336,10 @@ pub extern "C" fn voicevox_open_jtalk_rc_use_user_dict(
/// voicevox_open_jtalk_rc_delete(open_jtalk);
/// ```
/// }
-///
-/// \safety{
-/// - `open_jtalk`は ::voicevox_open_jtalk_rc_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。
-/// - `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: *mut OpenJtalkRc) {
init_logger_once();
- drop(open_jtalk);
+ open_jtalk.drop_body();
}
/// ハードウェアアクセラレーションモードを設定する設定値。
@@ -396,9 +396,10 @@ 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(Debug, Educe)]
+#[educe(Default(expression = "Self { _padding: MaybeUninit::uninit() }"))]
pub struct VoicevoxVoiceModelFile {
- model: voicevox_core::blocking::VoiceModelFile,
+ _padding: MaybeUninit<[u8; 1]>,
}
/// 音声モデルID。
@@ -423,12 +424,12 @@ 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((|| {
let path = ensure_utf8(CStr::from_ptr(path))?;
- let model = VoicevoxVoiceModelFile::open(path)?.into();
+ let model = VoicevoxVoiceModelFile::open(path)?;
out_model.write_unaligned(model);
Ok(())
})())
@@ -440,16 +441,15 @@ pub unsafe extern "C" fn voicevox_voice_model_file_open(
/// @param [out] output_voice_model_id 音声モデルID
///
/// \safety{
-/// - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。
/// - `output_voice_model_id`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_voice_model_file_id(
- model: &VoicevoxVoiceModelFile,
+ model: *const VoicevoxVoiceModelFile,
output_voice_model_id: NonNull<[u8; 16]>,
) {
init_logger_once();
- let id = model.model.id().raw_voice_model_id().into_bytes();
+ let id = model.body().id().raw_voice_model_id().into_bytes();
unsafe { output_voice_model_id.write_unaligned(id) };
}
@@ -460,13 +460,9 @@ pub unsafe extern "C" fn voicevox_voice_model_file_id(
/// @param [in] model 音声モデル
///
/// @returns メタ情報のJSON文字列
-///
-/// \safety{
-/// - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_voice_model_file_close で解放されていてはいけない。
-/// }
#[no_mangle]
pub extern "C" fn voicevox_voice_model_file_create_metas_json(
- model: &VoicevoxVoiceModelFile,
+ model: *const VoicevoxVoiceModelFile,
) -> *mut c_char {
init_logger_once();
C_STRING_DROP_CHECKER.whitelist(model.metas()).into_raw()
@@ -474,24 +470,24 @@ pub extern "C" fn voicevox_voice_model_file_create_metas_json(
/// ::VoicevoxVoiceModelFile を、所有しているファイルディスクリプタを閉じた上で破棄(_destruct_)する。
///
-/// @param [in] model 破棄対象
+/// 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
///
-/// \safety{
-/// - `model`は ::voicevox_voice_model_file_open で得たものでなければならず、また既にこの関数で解放されていてはいけない。
-/// - `model`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。
-/// }
+/// この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+///
+/// @param [in] model 破棄対象
#[no_mangle]
-pub extern "C" fn voicevox_voice_model_file_close(model: Box) {
+pub extern "C" fn voicevox_voice_model_file_close(model: *mut VoicevoxVoiceModelFile) {
init_logger_once();
- drop(model);
+ model.drop_body();
}
/// 音声シンセサイザ。
///
/// 構築(_construction_)は ::voicevox_synthesizer_new で行い、破棄(_destruction_)は ::voicevox_synthesizer_delete で行う。
-#[derive(Getters)]
+#[derive(Debug, Educe)]
+#[educe(Default(expression = "Self { _padding: MaybeUninit::uninit() }"))]
pub struct VoicevoxSynthesizer {
- synthesizer: voicevox_core::blocking::Synthesizer,
+ _padding: MaybeUninit<[u8; 1]>,
}
/// ::VoicevoxSynthesizer を構築(_construct_)する。
@@ -505,21 +501,20 @@ pub struct VoicevoxSynthesizer {
///
/// \safety{
/// - `onnxruntime`は ::voicevox_onnxruntime_load_once または ::voicevox_onnxruntime_init_once で得たものでなければならない。
-/// - `open_jtalk`は ::voicevox_voice_model_file_open で得たものでなければならず、また ::voicevox_open_jtalk_rc_new で解放されていてはいけない。
/// - `out_synthesizer`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_new(
onnxruntime: &'static VoicevoxOnnxruntime,
- open_jtalk: &OpenJtalkRc,
+ open_jtalk: *const OpenJtalkRc,
options: VoicevoxInitializeOptions,
- out_synthesizer: NonNull>,
+ out_synthesizer: NonNull>,
) -> VoicevoxResultCode {
init_logger_once();
into_result_code_with_error((|| {
let options = options.into();
- let synthesizer = VoicevoxSynthesizer::new(onnxruntime, open_jtalk, &options)?.into();
+ let synthesizer = VoicevoxSynthesizer::new(onnxruntime, open_jtalk, &options)?;
out_synthesizer.write_unaligned(synthesizer);
Ok(())
})())
@@ -527,16 +522,15 @@ pub unsafe extern "C" fn voicevox_synthesizer_new(
/// ::VoicevoxSynthesizer を破棄(_destruct_)する。
///
-/// @param [in] synthesizer 破棄対象
+/// 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
///
-/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。
-/// - `synthesizer`は以後ダングリングポインタ(_dangling pointer_)として扱われなくてはならない。
-/// }
+/// この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+///
+/// @param [in] synthesizer 破棄対象
#[no_mangle]
-pub extern "C" fn voicevox_synthesizer_delete(synthesizer: Box) {
+pub extern "C" fn voicevox_synthesizer_delete(synthesizer: *mut VoicevoxSynthesizer) {
init_logger_once();
- drop(synthesizer);
+ synthesizer.drop_body();
}
/// 音声モデルを読み込む。
@@ -545,18 +539,13 @@ pub extern "C" fn voicevox_synthesizer_delete(synthesizer: Box 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()))
}
/// 音声モデルの読み込みを解除する。
@@ -567,12 +556,11 @@ pub extern "C" fn voicevox_synthesizer_load_voice_model(
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `model_id`は読み込みについて有効でなければならない。
/// }
#[no_mangle]
pub extern "C" fn voicevox_synthesizer_unload_voice_model(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
model_id: VoicevoxVoiceModelId<'_>,
) -> VoicevoxResultCode {
init_logger_once();
@@ -585,13 +573,9 @@ pub extern "C" fn voicevox_synthesizer_unload_voice_model(
/// @param [in] synthesizer 音声シンセサイザ
///
/// @returns ::VoicevoxOnnxruntime のインスタンス
-///
-/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
-/// }
#[no_mangle]
pub extern "C" fn voicevox_synthesizer_get_onnxruntime(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
) -> &'static VoicevoxOnnxruntime {
synthesizer.onnxruntime()
}
@@ -601,14 +585,12 @@ pub extern "C" fn voicevox_synthesizer_get_onnxruntime(
/// @param [in] synthesizer 音声シンセサイザ
///
/// @returns GPUモードかどうか
-///
-/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
-/// }
#[no_mangle]
-pub extern "C" fn voicevox_synthesizer_is_gpu_mode(synthesizer: &VoicevoxSynthesizer) -> bool {
+pub extern "C" fn voicevox_synthesizer_is_gpu_mode(
+ synthesizer: *const VoicevoxSynthesizer,
+) -> bool {
init_logger_once();
- synthesizer.synthesizer().is_gpu_mode()
+ synthesizer.body().is_gpu_mode()
}
/// 指定したIDの音声モデルが読み込まれているか判定する。
@@ -619,17 +601,16 @@ pub extern "C" fn voicevox_synthesizer_is_gpu_mode(synthesizer: &VoicevoxSynthes
/// @returns モデルが読み込まれているかどうか
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `model_id`は読み込みについて有効でなければならない。
/// }
#[no_mangle]
pub extern "C" fn voicevox_synthesizer_is_loaded_voice_model(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
model_id: VoicevoxVoiceModelId<'_>,
) -> bool {
init_logger_once();
let model_id = model_id.to_model_id();
- synthesizer.synthesizer().is_loaded_voice_model(model_id)
+ synthesizer.body().is_loaded_voice_model(model_id)
}
/// 今読み込んでいる音声モデルのメタ情報を、JSONで取得する。
@@ -639,13 +620,9 @@ pub extern "C" fn voicevox_synthesizer_is_loaded_voice_model(
/// @param [in] synthesizer 音声シンセサイザ
///
/// @return メタ情報のJSON文字列
-///
-/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
-/// }
#[no_mangle]
pub extern "C" fn voicevox_synthesizer_create_metas_json(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
) -> *mut c_char {
init_logger_once();
let metas = synthesizer.metas();
@@ -713,13 +690,12 @@ pub unsafe extern "C" fn voicevox_onnxruntime_create_supported_devices_json(
/// }
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `kana`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_create_audio_query_from_kana(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
kana: *const c_char,
style_id: VoicevoxStyleId,
output_audio_query_json: NonNull<*mut c_char>,
@@ -730,7 +706,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'");
@@ -761,13 +737,12 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_audio_query_from_kana(
/// }
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `text`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_create_audio_query(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
text: *const c_char,
style_id: VoicevoxStyleId,
output_audio_query_json: NonNull<*mut c_char>,
@@ -778,7 +753,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'");
@@ -810,13 +785,12 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_audio_query(
/// }
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `kana`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_create_accent_phrases_from_kana(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
kana: *const c_char,
style_id: VoicevoxStyleId,
output_accent_phrases_json: NonNull<*mut c_char>,
@@ -825,7 +799,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'");
@@ -856,13 +830,12 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_accent_phrases_from_kana(
/// }
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `text`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_create_accent_phrases(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
text: *const c_char,
style_id: VoicevoxStyleId,
output_accent_phrases_json: NonNull<*mut c_char>,
@@ -871,7 +844,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'");
@@ -893,13 +866,12 @@ pub unsafe extern "C" fn voicevox_synthesizer_create_accent_phrases(
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `accent_phrases_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_replace_mora_data(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
accent_phrases_json: *const c_char,
style_id: VoicevoxStyleId,
output_accent_phrases_json: NonNull<*mut c_char>,
@@ -910,7 +882,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'");
@@ -932,13 +904,12 @@ pub unsafe extern "C" fn voicevox_synthesizer_replace_mora_data(
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `accent_phrases_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_replace_phoneme_length(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
accent_phrases_json: *const c_char,
style_id: VoicevoxStyleId,
output_accent_phrases_json: NonNull<*mut c_char>,
@@ -949,7 +920,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'");
@@ -971,13 +942,12 @@ pub unsafe extern "C" fn voicevox_synthesizer_replace_phoneme_length(
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `accent_phrases_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_audio_query_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_replace_mora_pitch(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
accent_phrases_json: *const c_char,
style_id: VoicevoxStyleId,
output_accent_phrases_json: NonNull<*mut c_char>,
@@ -988,7 +958,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'");
@@ -1027,14 +997,13 @@ pub extern "C" fn voicevox_make_default_synthesis_options() -> VoicevoxSynthesis
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `audio_query_json`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_wav_length`は書き込みについて有効でなければならない。
/// - `output_wav`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_synthesis(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
audio_query_json: *const c_char,
style_id: VoicevoxStyleId,
options: VoicevoxSynthesisOptions,
@@ -1048,7 +1017,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),
@@ -1087,14 +1056,13 @@ pub extern "C" fn voicevox_make_default_tts_options() -> VoicevoxTtsOptions {
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `kana`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_wav_length`は書き込みについて有効でなければならない。
/// - `output_wav`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_tts_from_kana(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
kana: *const c_char,
style_id: VoicevoxStyleId,
options: VoicevoxTtsOptions,
@@ -1104,7 +1072,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),
@@ -1128,14 +1096,13 @@ pub unsafe extern "C" fn voicevox_synthesizer_tts_from_kana(
/// @returns 結果コード
///
/// \safety{
-/// - `synthesizer`は ::voicevox_synthesizer_new で得たものでなければならず、また ::voicevox_synthesizer_delete で解放されていてはいけない。
/// - `text`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_wav_length`は書き込みについて有効でなければならない。
/// - `output_wav`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_synthesizer_tts(
- synthesizer: &VoicevoxSynthesizer,
+ synthesizer: *const VoicevoxSynthesizer,
text: *const c_char,
style_id: VoicevoxStyleId,
options: VoicevoxTtsOptions,
@@ -1145,11 +1112,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(())
})())
@@ -1227,9 +1193,10 @@ pub extern "C" fn voicevox_error_result_to_message(
}
/// ユーザー辞書。
-#[derive(Default)]
+#[derive(Debug, Educe)]
+#[educe(Default(expression = "Self { _padding: MaybeUninit::uninit() }"))]
pub struct VoicevoxUserDict {
- dict: Arc,
+ _padding: MaybeUninit<[u8; 1]>,
}
/// ユーザー辞書の単語。
@@ -1292,9 +1259,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())
}
/// ユーザー辞書にファイルを読み込ませる。
@@ -1304,18 +1271,17 @@ pub extern "C" fn voicevox_user_dict_new() -> Box {
/// @returns 結果コード
///
/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
/// - `dict_path`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_user_dict_load(
- user_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
dict_path: *const c_char,
) -> VoicevoxResultCode {
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(())
})())
@@ -1332,20 +1298,19 @@ pub unsafe extern "C" fn voicevox_user_dict_load(
/// @param user_dict は有効な :VoicevoxUserDict のポインタであること
///
/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
/// - `word->surface`と`word->pronunciation`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// - `output_word_uuid`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_user_dict_add_word(
- user_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
word: *const VoicevoxUserDictWord,
output_word_uuid: NonNull<[u8; 16]>,
) -> VoicevoxResultCode {
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(())
@@ -1360,13 +1325,12 @@ pub unsafe extern "C" fn voicevox_user_dict_add_word(
/// @returns 結果コード
///
/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
/// - `word_uuid`は読み込みについて有効でなければならない。
/// - `word->surface`と`word->pronunciation`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_user_dict_update_word(
- user_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
word_uuid: &[u8; 16],
word: *const VoicevoxUserDictWord,
) -> VoicevoxResultCode {
@@ -1374,7 +1338,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(())
})())
@@ -1387,18 +1351,17 @@ pub unsafe extern "C" fn voicevox_user_dict_update_word(
/// @returns 結果コード
///
/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
/// - `word_uuid`は読み込みについて有効でなければならない。
/// }
#[no_mangle]
pub extern "C" fn voicevox_user_dict_remove_word(
- user_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
word_uuid: &[u8; 16],
) -> VoicevoxResultCode {
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(())
})())
}
@@ -1413,16 +1376,15 @@ pub extern "C" fn voicevox_user_dict_remove_word(
/// @returns 結果コード
///
/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
/// - `output_json`は書き込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_user_dict_to_json(
- user_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
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
@@ -1433,18 +1395,14 @@ pub unsafe extern "C" fn voicevox_user_dict_to_json(
/// @param [in] user_dict ユーザー辞書
/// @param [in] other_dict インポートするユーザー辞書
/// @returns 結果コード
-///
-/// \safety{
-/// - `user_dict`と`other_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
-/// }
#[no_mangle]
pub extern "C" fn voicevox_user_dict_import(
- user_dict: &VoicevoxUserDict,
- other_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
+ other_dict: *const VoicevoxUserDict,
) -> VoicevoxResultCode {
init_logger_once();
into_result_code_with_error((|| {
- user_dict.dict.import(&other_dict.dict)?;
+ user_dict.body().import(&other_dict.body())?;
Ok(())
})())
}
@@ -1455,31 +1413,30 @@ pub extern "C" fn voicevox_user_dict_import(
/// @param [in] path 保存先のファイルパス
///
/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また ::voicevox_user_dict_delete で解放されていてはいけない。
/// - `path`はヌル終端文字列を指し、かつ読み込みについて有効でなければならない。
/// }
#[no_mangle]
pub unsafe extern "C" fn voicevox_user_dict_save(
- user_dict: &VoicevoxUserDict,
+ user_dict: *const VoicevoxUserDict,
path: *const c_char,
) -> VoicevoxResultCode {
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(())
})())
}
/// ユーザー辞書を破棄(_destruct_)する。
///
-/// @param [in] user_dict 破棄対象
+/// 破棄対象への他スレッドでのアクセスが存在する場合、それらがすべて終わるのを待ってから破棄する。
///
-/// \safety{
-/// - `user_dict`は ::voicevox_user_dict_new で得たものでなければならず、また既にこの関数で解放されていてはいけない。
-/// }
+/// この関数の呼び出し後に破棄し終えた対象にアクセスすると、プロセスを異常終了する。
+///
+/// @param [in] user_dict 破棄対象
#[no_mangle]
-pub unsafe extern "C" fn voicevox_user_dict_delete(user_dict: Box) {
+pub extern "C" fn voicevox_user_dict_delete(user_dict: *mut VoicevoxUserDict) {
init_logger_once();
- drop(user_dict);
+ user_dict.drop_body();
}
diff --git a/crates/voicevox_core_c_api/src/object.rs b/crates/voicevox_core_c_api/src/object.rs
new file mode 100644
index 000000000..a3aea19f1
--- /dev/null
+++ b/crates/voicevox_core_c_api/src/object.rs
@@ -0,0 +1,145 @@
+use std::{
+ any,
+ collections::HashMap,
+ fmt::{Debug, Display},
+ mem,
+ ops::{Deref, DerefMut},
+ ptr::NonNull,
+ sync::Arc,
+};
+
+use easy_ext::ext;
+use tracing::warn;
+
+/// プロセスの終わりまでデストラクトされない、ユーザーにオブジェクトとして貸し出す1-bit長の構造体。
+///
+/// インスタンスは次のような形。
+///
+/// ```
+/// pub struct VoicevoxSynthesizer {
+/// _padding: MaybeUninit<[u8; 1]>,
+/// }
+/// ```
+///
+/// `RustApiObject`そのものではなくこのトレイトのインスタンスをユーザーに渡すようにすることで、次のことを実現する。
+///
+/// 1. "delete"時に対象オブジェクトに対するアクセスがあった場合、アクセスが終わるまで待つ
+/// 2. 次のユーザー操作に対するセーフティネットを張り、パニックするようにする
+/// 1. "delete"後に他の通常のメソッド関数の利用を試みる
+/// 2. "delete"後に"delete"を試みる
+/// 3. そもそもオブジェクトとして変なダングリングポインタが渡される
+pub(crate) trait CApiObject: Default + Debug + 'static {
+ type RustApiObject: 'static;
+
+ // 書き込み操作としては`push`のみ
+ fn heads() -> &'static std::sync::Mutex>;
+
+ #[expect(
+ clippy::type_complexity,
+ reason = "型を分離するとかえって可読性を失う。その代わりコメントを入れている"
+ )]
+ fn bodies() -> &'static std::sync::Mutex<
+ HashMap<
+ usize, // `heads`の要素へのポインタのアドレス
+ Arc<
+ parking_lot::RwLock<
+ Option, // `RwLock`をdropする直前まで`Some`
+ >,
+ >,
+ >,
+ >;
+
+ fn new(body: Self::RustApiObject) -> NonNull {
+ assert!(mem::size_of::() > 0);
+
+ let this = {
+ let mut heads = Self::lock_heads();
+ heads.push(Default::default());
+ NonNull::from(heads.last().expect("just pushed"))
+ };
+ let body = parking_lot::RwLock::new(body.into()).into();
+ Self::lock_bodies().insert(this.as_ptr() as _, body);
+ this
+ }
+}
+
+#[ext(CApiObjectPtrExt)]
+impl *const T {
+ /// # Panics
+ ///
+ /// 同じ対象に対して`drop_body`を呼んでいるとパニックする。
+ pub(crate) fn body(self) -> impl Deref {
+ self.validate();
+
+ let body = T::lock_bodies()
+ .get(&(self as _))
+ .unwrap_or_else(|| self.panic_for_deleted())
+ .read_arc();
+
+ voicevox_core::__internal::interop::raii::try_map_guard(body, |body| {
+ body.as_ref().ok_or(())
+ })
+ .unwrap_or_else(|()| self.panic_for_deleted())
+ }
+
+ /// # Panics
+ ///
+ /// 同じ対象に対してこの関数を二度呼ぶとパニックする。
+ pub(crate) fn drop_body(self) {
+ self.validate();
+
+ let body = T::lock_bodies()
+ .remove(&(self as _))
+ .unwrap_or_else(|| self.panic_for_deleted());
+
+ drop(
+ body.try_write_arc()
+ .unwrap_or_else(|| {
+ warn!(
+ "{this} is still in use. Waiting before closing",
+ this = self.display(),
+ );
+ body.write_arc()
+ })
+ .take()
+ .unwrap_or_else(|| self.panic_for_deleted()),
+ );
+ }
+}
+
+#[ext]
+impl *const T {
+ fn validate(self) {
+ if self.is_null() {
+ panic!("the argument must not be null");
+ }
+ if !T::lock_heads().as_ptr_range().contains(&self) {
+ panic!("{self:018p} does not seem to be valid object");
+ }
+ }
+
+ fn display(self) -> impl Display {
+ let type_name = any::type_name::()
+ .split("::")
+ .last()
+ .expect("should not empty");
+ format!("`{type_name}` ({self:018p})")
+ }
+
+ fn panic_for_deleted(self) -> ! {
+ panic!("{}は既に破棄されています", self.display());
+ }
+}
+
+#[ext]
+impl T {
+ fn lock_heads() -> impl DerefMut> {
+ Self::heads().lock().unwrap_or_else(|e| panic!("{e}"))
+ }
+
+ fn lock_bodies(
+ ) -> impl DerefMut>>>>
+ {
+ Self::bodies().lock().unwrap_or_else(|e| panic!("{e}"))
+ }
+}
diff --git a/crates/voicevox_core_c_api/tests/e2e/snapshots.toml b/crates/voicevox_core_c_api/tests/e2e/snapshots.toml
index eb5911c7e..602824543 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_matches_all = [
+ '\n`OpenJtalkRc` \(0x[0-9a-f]{16}\)は既に破棄されています\n',
+ "\nthread caused non-unwinding panic. aborting.\n",
+]
+
+[double_delete_synthesizer]
+stderr_matches_all = [
+ '\n`VoicevoxSynthesizer` \(0x[0-9a-f]{16}\)は既に破棄されています\n',
+ "\nthread caused non-unwinding panic. aborting.\n",
+]
+
+[double_delete_user_dict]
+stderr_matches_all = [
+ '\n`VoicevoxUserDict` \(0x[0-9a-f]{16}\)は既に破棄されています\n',
+ "\nthread caused non-unwinding panic. aborting.\n",
+]
+
+[double_delete_voice_model_file]
+stderr_matches_all = [
+ '\n`VoicevoxVoiceModelFile` \(0x[0-9a-f]{16}\)は既に破棄されています\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..511ae3b41
--- /dev/null
+++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_openjtalk.rs
@@ -0,0 +1,62 @@
+//! `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_matches_all {
+ let p = predicates::str::is_match(s).unwrap_or_else(|e| panic!("{e}"));
+ assert = assert.try_stderr(p)?;
+ }
+ Ok(assert)
+ }
+}
+
+static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_openjtalk);
+
+#[derive(Deserialize)]
+struct Snapshots {
+ stderr_matches_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..170ee718f
--- /dev/null
+++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_synthesizer.rs
@@ -0,0 +1,86 @@
+//! `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_matches_all {
+ let p = predicates::str::is_match(s).unwrap_or_else(|e| panic!("{e}"));
+ assert = assert.try_stderr(p)?;
+ }
+ Ok(assert)
+ }
+}
+
+static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_synthesizer);
+
+#[derive(Deserialize)]
+struct Snapshots {
+ stderr_matches_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..6256b63e0
--- /dev/null
+++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_user_dict.rs
@@ -0,0 +1,47 @@
+//! `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_matches_all {
+ let p = predicates::str::is_match(s).unwrap_or_else(|e| panic!("{e}"));
+ assert = assert.try_stderr(p)?;
+ }
+ Ok(assert)
+ }
+}
+
+static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_user_dict);
+
+#[derive(Deserialize)]
+struct Snapshots {
+ stderr_matches_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..1ea6bdc19
--- /dev/null
+++ b/crates/voicevox_core_c_api/tests/e2e/testcases/double_delete_voice_model_file.rs
@@ -0,0 +1,59 @@
+//! `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_matches_all {
+ let p = predicates::str::is_match(s).unwrap_or_else(|e| panic!("{e}"));
+ assert = assert.try_stderr(p)?;
+ }
+ Ok(assert)
+ }
+}
+
+static SNAPSHOTS: LazyLock = snapshots::section!(double_delete_voice_model_file);
+
+#[derive(Deserialize)]
+struct Snapshots {
+ stderr_matches_all: IndexSet,
+}