Skip to content

Commit

Permalink
[project-vvm-async-api] "buffer"をRustの世界で保持し続ける (#525)
Browse files Browse the repository at this point in the history
Co-authored-by: Ryo Yamashita <[email protected]>
Co-authored-by: Hiroshiba <[email protected]>
  • Loading branch information
qryxip and Hiroshiba authored Jun 20, 2023
1 parent 0a0cf99 commit a00ff98
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 213 deletions.
126 changes: 126 additions & 0 deletions crates/voicevox_core_c_api/src/drop_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::{
collections::BTreeSet,
ffi::{c_char, CStr, CString},
sync::Mutex,
};

/// dropして良い`*mut c_char`を把握し、チェックする。
///
/// `Mutex`による内部可変性を持ち、すべての操作は共有参照から行うことができる。
///
/// # Motivation
///
/// `CString`は`Box<impl Sized>`と同様Cの世界でもポインタ一つで実体を表すことができるため、こちら側
/// で管理すべきものは本来無い。しかしながら本クレートが提供するAPIには「解放不要」な文字列を返すも
/// のが含まれている。ユーザーが誤ってそのような文字列を解放するのは未定義動作 (undefined behavior)
/// であるため、綺麗にSEGVするとも限らない。`once_cell::sync::Lazy`由来の文字列の場合、最悪解放が成
/// 功してしまう。
///
/// この構造体はCの世界から帰ってきた`*mut c_char`を`CString`としてdropする際、それが本当にこちら側
/// が送り出した`CString`かどうかをチェックする。
///
/// Cの世界に`CString`を送り出す前に`whitelist`を通し、戻って来た`*mut c_char`を`CString`にしてdrop
/// する前に`check`に通す。
pub(crate) static C_STRING_DROP_CHECKER: CStringDropChecker = CStringDropChecker::new();

pub(crate) struct CStringDropChecker(Mutex<Inner>);

struct Inner {
owned_str_addrs: BTreeSet<usize>,
static_str_addrs: BTreeSet<usize>,
}

impl CStringDropChecker {
const fn new() -> Self {
Self(Mutex::new(Inner {
owned_str_addrs: BTreeSet::new(),
static_str_addrs: BTreeSet::new(),
}))
}

/// `CString`をホワイトリストに追加する。
///
/// Cの世界に`CString`を送り出す前にこの関数を挟む。
pub(crate) fn whitelist(&self, s: CString) -> CString {
let Inner {
owned_str_addrs, ..
} = &mut *self.0.lock().unwrap();

let duplicated = !owned_str_addrs.insert(s.as_ptr() as usize);
assert!(!duplicated, "duplicated");
s
}

/// `&'static CStr`をブラックリストに追加する。
///
/// Cの世界に`Lazy`由来の`&'static CStr`を送り出す前にこの関数を挟む。
///
/// ホワイトリストとブラックリストは重複しないと考えてよく、ブラックリストはエラーメセージの制御
/// のためのみに使われる。
pub(crate) fn blacklist(&self, s: &'static CStr) -> &'static CStr {
let Inner {
static_str_addrs, ..
} = &mut *self.0.lock().unwrap();

static_str_addrs.insert(s.as_ptr() as usize);
s
}

/// `*mut c_char`が`whitelist`を通ったものかどうかチェックする。
///
/// # Panics
///
/// `ptr`が`Self::whitelist`を経由したものではないならパニックする。
pub(crate) fn check(&self, ptr: *mut c_char) -> *mut c_char {
let Inner {
owned_str_addrs,
static_str_addrs,
..
} = &mut *self.0.lock().unwrap();

let addr = ptr as usize;
if !owned_str_addrs.remove(&addr) {
if static_str_addrs.contains(&addr) {
panic!(
"解放しようとしたポインタはvoicevox_core管理下のものですが、\
voicevox_coreがアンロードされるまで永続する文字列に対するものです。\
解放することはできません",
)
}
panic!(
"解放しようとしたポインタはvoicevox_coreの管理下にありません。\
誤ったポインタであるか、二重解放になっていることが考えられます",
);
}
ptr
}
}

#[cfg(test)]
mod tests {
use std::ffi::{c_char, CStr};

use super::CStringDropChecker;

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_coreの管理下にありません。誤ったポインタであるか、二重解放になっていることが考えられます"
)]
fn it_denies_unknown_char_ptr() {
let checker = CStringDropChecker::new();
let s = CStr::from_bytes_with_nul(b"\0").unwrap().to_owned();
checker.check(s.into_raw());
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_core管理下のものですが、voicevox_coreがアンロードされるまで永続する文字列に対するものです。解放することはできません"
)]
fn it_denies_known_static_char_ptr() {
let checker = CStringDropChecker::new();
checker.blacklist(STATIC);
checker.check(STATIC.as_ptr() as *mut c_char);

static STATIC: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") };
}
}
193 changes: 0 additions & 193 deletions crates/voicevox_core_c_api/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::alloc::Layout;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Debug;

use const_default::ConstDefault;
Expand Down Expand Up @@ -177,194 +175,3 @@ impl ConstDefault for VoicevoxSynthesisOptions {
}
};
}

// libcのmallocで追加のアロケーションを行うことなく、`Vec<u8>`や`Vec<f32>`の内容を直接Cの世界に貸し出す。

/// Rustの世界の`Box<[impl Copy]>`をCの世界に貸し出すため、アドレスとレイアウトを管理するもの。
///
/// `Mutex`による内部可変性を持ち、すべての操作は`&self`から行うことができる。
pub(crate) struct BufferManager(Mutex<BufferManagerInner>);

struct BufferManagerInner {
address_to_layout_table: BTreeMap<usize, Layout>,
owned_str_addrs: BTreeSet<usize>,
static_str_addrs: BTreeSet<usize>,
}

impl BufferManager {
pub const fn new() -> Self {
Self(Mutex::new(BufferManagerInner {
address_to_layout_table: BTreeMap::new(),
owned_str_addrs: BTreeSet::new(),
static_str_addrs: BTreeSet::new(),
}))
}

pub fn vec_into_raw<T: Copy>(&self, vec: Vec<T>) -> (*mut T, usize) {
let BufferManagerInner {
address_to_layout_table,
..
} = &mut *self.0.lock().unwrap();

let slice = Box::leak(vec.into_boxed_slice());
let layout = Layout::for_value(slice);
let len = slice.len();
let ptr = slice.as_mut_ptr();
let addr = ptr as usize;

let not_occupied = address_to_layout_table.insert(addr, layout).is_none();

assert!(not_occupied, "すでに値が入っている状態はおかしい");

(ptr, len)
}

/// `vec_into_raw`でC API利用側に貸し出したポインタに対し、デアロケートする。
///
/// # Safety
///
/// - `buffer_ptr`は`vec_into_raw`で取得したものであること。
pub unsafe fn dealloc_slice<T: Copy>(&self, buffer_ptr: *const T) {
let BufferManagerInner {
address_to_layout_table,
..
} = &mut *self.0.lock().unwrap();

let addr = buffer_ptr as usize;
let layout = address_to_layout_table.remove(&addr).expect(
"解放しようとしたポインタはvoicevox_coreの管理下にありません。\
誤ったポインタであるか、二重解放になっていることが考えられます",
);

if layout.size() > 0 {
// `T: Copy`より、`T: !Drop`であるため`drop_in_place`は不要

// SAFETY:
// - `addr`と`layout`は対応したものである
// - `layout.size() > 0`より、`addr`はダングリングではない有効なポインタである
std::alloc::dealloc(addr as *mut u8, layout);
}
}

pub fn c_string_into_raw(&self, s: CString) -> *mut c_char {
let BufferManagerInner {
owned_str_addrs, ..
} = &mut *self.0.lock().unwrap();

let ptr = s.into_raw();
owned_str_addrs.insert(ptr as _);
ptr
}

/// `c_string_into_raw`でC API利用側に貸し出したポインタに対し、デアロケートする。
///
/// # Safety
///
/// - `ptr`は`c_string_into_raw`で取得したものであること。
pub unsafe fn dealloc_c_string(&self, ptr: *mut c_char) {
let BufferManagerInner {
owned_str_addrs,
static_str_addrs,
..
} = &mut *self.0.lock().unwrap();

if !owned_str_addrs.remove(&(ptr as _)) {
if static_str_addrs.contains(&(ptr as _)) {
panic!(
"解放しようとしたポインタはvoicevox_core管理下のものですが、\
voicevox_coreがアンロードされるまで永続する文字列に対するものです。\
解放することはできません",
)
}
panic!(
"解放しようとしたポインタはvoicevox_coreの管理下にありません。\
誤ったポインタであるか、二重解放になっていることが考えられます",
);
}
drop(CString::from_raw(ptr));
}

pub fn memorize_static_str(&self, ptr: *const c_char) -> *const c_char {
let BufferManagerInner {
static_str_addrs, ..
} = &mut *self.0.lock().unwrap();

static_str_addrs.insert(ptr as _);
ptr
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn buffer_manager_works() {
let mut buffer_manager = BufferManager::new();

rent_and_dealloc(&mut buffer_manager, vec::<()>(0, &[]));
rent_and_dealloc(&mut buffer_manager, vec(0, &[()]));
rent_and_dealloc(&mut buffer_manager, vec(2, &[()]));

rent_and_dealloc(&mut buffer_manager, vec::<u8>(0, &[]));
rent_and_dealloc(&mut buffer_manager, vec(0, &[0u8]));
rent_and_dealloc(&mut buffer_manager, vec(2, &[0u8]));

rent_and_dealloc(&mut buffer_manager, vec::<f32>(0, &[]));
rent_and_dealloc(&mut buffer_manager, vec(0, &[0f32]));
rent_and_dealloc(&mut buffer_manager, vec(2, &[0f32]));

fn rent_and_dealloc(buffer_manager: &mut BufferManager, vec: Vec<impl Copy>) {
let expected_len = vec.len();
let (ptr, len) = buffer_manager.vec_into_raw(vec);
assert_eq!(expected_len, len);
unsafe {
buffer_manager.dealloc_slice(ptr);
}
}

fn vec<T: Copy>(initial_cap: usize, elems: &[T]) -> Vec<T> {
let mut vec = Vec::with_capacity(initial_cap);
vec.extend_from_slice(elems);
vec
}
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_coreの管理下にありません。誤ったポインタであるか、二重解放になっていることが考えられます"
)]
fn buffer_manager_denies_unknown_slice_ptr() {
let buffer_manager = BufferManager::new();
unsafe {
let x = 42;
buffer_manager.dealloc_slice(&x as *const i32);
}
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_coreの管理下にありません。誤ったポインタであるか、二重解放になっていることが考えられます"
)]
fn buffer_manager_denies_unknown_char_ptr() {
let buffer_manager = BufferManager::new();
unsafe {
let s = CStr::from_bytes_with_nul(b"\0").unwrap().to_owned();
buffer_manager.dealloc_c_string(s.into_raw());
}
}

#[test]
#[should_panic(
expected = "解放しようとしたポインタはvoicevox_core管理下のものですが、voicevox_coreがアンロードされるまで永続する文字列に対するものです。解放することはできません"
)]
fn buffer_manager_denies_known_static_char_ptr() {
let buffer_manager = BufferManager::new();
unsafe {
buffer_manager.memorize_static_str(STATIC.as_ptr() as _);
buffer_manager.dealloc_c_string(STATIC.as_ptr() as *mut c_char);
}

static STATIC: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") };
}
}
Loading

0 comments on commit a00ff98

Please sign in to comment.