From a3a871716fa4cc5aab95f37010dba0a69a2d2d01 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 00:23:36 +0000 Subject: [PATCH 01/44] FFI: Create ffi Cargo feature and include ffi module. --- Cargo.toml | 2 ++ src/ffi.rs | 0 src/lib.rs | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 src/ffi.rs diff --git a/Cargo.toml b/Cargo.toml index fd7424c..c5f70d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,12 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] use_tolk = ["tolk"] +ffi = ["libc"] [dependencies] dyn-clonable = "0.9" lazy_static = "1" +libc = {version = "0.2", optional = true} log = "0.4" thiserror = "1" diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index 820ccd3..9256a53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ use objc::{class, msg_send, sel, sel_impl}; use thiserror::Error; mod backends; +#[cfg(feature = "ffi")] +pub mod ffi; #[derive(Clone, Copy, Debug)] pub enum Backends { From 9471a2086a143a31b65ede9e29c63974cf09cde1 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 01:13:40 +0000 Subject: [PATCH 02/44] FFI: Create error handling code and LAST_ERROR static Any errors reported will cause the C API functions to return an error value (NULL or -1). The caller can then use: * const char* tts_get_error() to get a pointer to a string describing the error * void tts_clear_error() to deallocate any currently stored error message. --- src/ffi.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index e69de29..5a2e286 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -0,0 +1,37 @@ +use libc::c_char; +use std::{ + cell::RefCell, + ffi::{CString, NulError}, + ptr, +}; + +thread_local! { + /// Stores the last reported error, so it can be retrieved at will from C + static LAST_ERROR: RefCell> = RefCell::new(None); +} + +fn set_last_error>>(err: E) -> Result<(), NulError> { + LAST_ERROR.with(|last| { + *last.borrow_mut() = Some(CString::new(err)?); + Ok(()) + }) +} + +/// Get the last reported error as a const C string (const char*) +/// This string will be valid until at least the next call to `tts_get_error`. +/// It is never called internally by the library. +#[no_mangle] +pub extern "C" fn tts_get_error() -> *const c_char { + LAST_ERROR.with(|err| match &*err.borrow() { + Some(e) => e.as_ptr(), + None => ptr::null(), + }) +} + +/// Deallocate the last reported error (if any). +#[no_mangle] +pub extern "C" fn tts_clear_error() { + LAST_ERROR.with(|err| { + *err.borrow_mut() = None; + }); +} From ab558cbbd2da413d9343b18b3e9d6bae3914e46c Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 01:40:13 +0000 Subject: [PATCH 03/44] FFI: Implement tts_default() constructor and tts_free() destructor * tts_default() allocates a new TTS struct via it's default() constructor, returning a pointer to it or NULL on error. * tts_free(tts) destroys the TTS pointed to by tts. If tts is NULL, this function does nothing. --- src/ffi.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index 5a2e286..71f14d3 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -5,6 +5,8 @@ use std::{ ptr, }; +use crate::TTS; + thread_local! { /// Stores the last reported error, so it can be retrieved at will from C static LAST_ERROR: RefCell> = RefCell::new(None); @@ -35,3 +37,29 @@ pub extern "C" fn tts_clear_error() { *err.borrow_mut() = None; }); } + +/// Creates a new TTS object with the default backend and returns a pointer to it. +/// If an error occured, a null pointer is returned, +/// Call `tts_get_error()` for more information about the specific error. +#[no_mangle] +pub extern "C" fn tts_default() -> *mut TTS { + match TTS::default() { + Ok(tts) => Box::into_raw(Box::new(tts)), + Err(e) => { + set_last_error(e.to_string()).unwrap(); + ptr::null_mut() + } + } +} + +/// Frees the memory associated with a TTS object. +/// If `tts` is a null pointer, this function does nothing. +#[no_mangle] +pub extern "C" fn tts_free(tts: *mut TTS) { + if tts.is_null() { + return; + } + unsafe { + Box::from_raw(tts); // Goes out of scope and is dropped + } +} From 56a2d554fa251bbda88d0eda452ade3b2cf3cf64 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 16:32:53 +0000 Subject: [PATCH 04/44] FFI: Implement tts_new(backend) This required giving the Backends enum and Features struct a C representation. --- src/ffi.rs | 16 +++++++++++++++- src/lib.rs | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/ffi.rs b/src/ffi.rs index 71f14d3..16cac9e 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -5,7 +5,7 @@ use std::{ ptr, }; -use crate::TTS; +use crate::{Backends, TTS}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -38,6 +38,20 @@ pub extern "C" fn tts_clear_error() { }); } +/// Creates a new TTS object with the specified backend and returns a pointer to it. +/// If an error occured, a null pointer is returned, +/// Call `tts_get_error()` for more information about the specific error. +#[no_mangle] +pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { + match TTS::new(backend) { + Ok(tts) => Box::into_raw(Box::new(tts)), + Err(e) => { + set_last_error(e.to_string()).unwrap(); + ptr::null_mut() + } + } +} + /// Creates a new TTS object with the default backend and returns a pointer to it. /// If an error occured, a null pointer is returned, /// Call `tts_get_error()` for more information about the specific error. diff --git a/src/lib.rs b/src/lib.rs index 9256a53..76aa3a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ mod backends; #[cfg(feature = "ffi")] pub mod ffi; +#[repr(C)] #[derive(Clone, Copy, Debug)] pub enum Backends { #[cfg(target_os = "linux")] @@ -75,6 +76,7 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} +#[repr(C)] pub struct Features { pub stop: bool, pub rate: bool, From 7c5ceb2bd3587cadb99127dee205920139de0075 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 16:35:08 +0000 Subject: [PATCH 05/44] FFI: Add cbindgen config for generation of C/C++ headers. --- cbindgen.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 cbindgen.toml diff --git a/cbindgen.toml b/cbindgen.toml new file mode 100644 index 0000000..7b647ff --- /dev/null +++ b/cbindgen.toml @@ -0,0 +1,8 @@ +include_guard = "TTS_RS_H" +autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" + +[defines] +"target_os = linux" = "__linux__" +"target_os = macos" = "__APPLE__" +"target_arch = wasm32" = "__EMSCRIPTEN__" +windows = "__WIN32__" From 195ccbeb23a98b69fe2fb2f2430d93339457f955 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 16:59:07 +0000 Subject: [PATCH 06/44] FFI: Allow determining TTS supported Features from C/C++ --- src/ffi.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ffi.rs b/src/ffi.rs index 16cac9e..693d9f3 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -5,7 +5,7 @@ use std::{ ptr, }; -use crate::{Backends, TTS}; +use crate::{Backends, Features, TTS}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -77,3 +77,10 @@ pub extern "C" fn tts_free(tts: *mut TTS) { Box::from_raw(tts); // Goes out of scope and is dropped } } + +/// Returns the features supported by this TTS engine. +/// tts must be a valid pointer to a TTS object. +#[no_mangle] +pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { + unsafe { tts.as_ref().unwrap().supported_features() } +} From c0f92500997ccd0912bb79e48a1a5ff21129a8d6 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 17:06:58 +0000 Subject: [PATCH 07/44] FFI: Update documentation comments to match their Rust equivilents. --- src/ffi.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 693d9f3..bb71589 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -38,8 +38,8 @@ pub extern "C" fn tts_clear_error() { }); } -/// Creates a new TTS object with the specified backend and returns a pointer to it. -/// If an error occured, a null pointer is returned, +/// Create a new `TTS` instance with the specified backend. +/// If an error occures, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { @@ -52,8 +52,8 @@ pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { } } -/// Creates a new TTS object with the default backend and returns a pointer to it. -/// If an error occured, a null pointer is returned, +/// Create a new TTS object with the default backend. +/// If an error occures, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] pub extern "C" fn tts_default() -> *mut TTS { @@ -66,7 +66,7 @@ pub extern "C" fn tts_default() -> *mut TTS { } } -/// Frees the memory associated with a TTS object. +/// Free the memory associated with a TTS object. /// If `tts` is a null pointer, this function does nothing. #[no_mangle] pub extern "C" fn tts_free(tts: *mut TTS) { @@ -79,7 +79,7 @@ pub extern "C" fn tts_free(tts: *mut TTS) { } /// Returns the features supported by this TTS engine. -/// tts must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a TTS object. #[no_mangle] pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { unsafe { tts.as_ref().unwrap().supported_features() } From 98bfc10d41107429ddb53deb725e1211ffc00445 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 19:11:05 +0000 Subject: [PATCH 08/44] FFI: Implement tts_speak() The tts_speak() function has an additional parameter that, if not NULL, will be filled with a pointer to an UtteranceId. If this is specified, the caller must also call tts_free_utterance() to deallocate the UtteranceId when they're done with it. --- src/ffi.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index bb71589..ec349e7 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,11 +1,11 @@ use libc::c_char; use std::{ cell::RefCell, - ffi::{CString, NulError}, + ffi::{CStr, CString, NulError}, ptr, }; -use crate::{Backends, Features, TTS}; +use crate::{Backends, Features, UtteranceId, TTS}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -19,7 +19,7 @@ fn set_last_error>>(err: E) -> Result<(), NulError> { }) } -/// Get the last reported error as a const C string (const char*) +/// Get the last reported error as a const C string. /// This string will be valid until at least the next call to `tts_get_error`. /// It is never called internally by the library. #[no_mangle] @@ -84,3 +84,49 @@ pub extern "C" fn tts_free(tts: *mut TTS) { pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { unsafe { tts.as_ref().unwrap().supported_features() } } + +/// Speaks the specified text, optionally interrupting current speech. +/// If `utterance` is not NULL, , fills it with a pointer to the returned UtteranceId (or NULL if +/// the backend doesn't provide one). +/// Returns true on success, false on error or if `tts` is NULL. +#[no_mangle] +pub extern "C" fn tts_speak( + tts: *mut TTS, + text: *const c_char, + interrupt: bool, + utterance: *mut *mut UtteranceId, +) -> bool { + if tts.is_null() { + return true; + } + unsafe { + let text = CStr::from_ptr(text).to_string_lossy().into_owned(); + match tts.as_mut().unwrap().speak(text, interrupt) { + Ok(u) => { + if !utterance.is_null() { + *utterance = match u { + Some(u) => Box::into_raw(Box::new(u)), + None => ptr::null_mut(), + }; + } + return true; + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + return false; + } + } + } +} + +/// Free the memory associated with an `UtteranceId`. +/// Does nothing if `utterance` is NULL. +#[no_mangle] +pub extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { + if utterance.is_null() { + return; + } + unsafe { + Box::from_raw(utterance); + } +} From ae6e64d973c8ef4e1df8eee01fbee560b1bac4ac Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 19:25:36 +0000 Subject: [PATCH 09/44] FFI: Implement tts_stop() function. --- src/ffi.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index ec349e7..6323ab6 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -130,3 +130,18 @@ pub extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { Box::from_raw(utterance); } } +/// Stops current speech. +/// Returns true on success, false on error or if `tts` is NULL. +#[no_mangle] +pub extern "C" fn tts_stop(tts: *mut TTS) -> bool { + if tts.is_null() { + return false; + } + match unsafe { tts.as_mut().unwrap().stop() } { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From f71529d9b67115277c0daa1c112c8ad25534e230 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 19:37:44 +0000 Subject: [PATCH 10/44] FFI: Make all functions with unsafe blocks completely unsafe. This is better than using unsafe blocks inside the functions, as that tells the compiler that the unsafeness won't leak out of the block, which isn't true in this case as we're dealing with another unsafe language. --- src/ffi.rs | 50 ++++++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 6323ab6..bf44363 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -69,20 +69,18 @@ pub extern "C" fn tts_default() -> *mut TTS { /// Free the memory associated with a TTS object. /// If `tts` is a null pointer, this function does nothing. #[no_mangle] -pub extern "C" fn tts_free(tts: *mut TTS) { +pub unsafe extern "C" fn tts_free(tts: *mut TTS) { if tts.is_null() { return; } - unsafe { - Box::from_raw(tts); // Goes out of scope and is dropped - } + Box::from_raw(tts); // Goes out of scope and is dropped } /// Returns the features supported by this TTS engine. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { - unsafe { tts.as_ref().unwrap().supported_features() } +pub unsafe extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { + tts.as_ref().unwrap().supported_features() } /// Speaks the specified text, optionally interrupting current speech. @@ -90,7 +88,7 @@ pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { /// the backend doesn't provide one). /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] -pub extern "C" fn tts_speak( +pub unsafe extern "C" fn tts_speak( tts: *mut TTS, text: *const c_char, interrupt: bool, @@ -99,22 +97,20 @@ pub extern "C" fn tts_speak( if tts.is_null() { return true; } - unsafe { - let text = CStr::from_ptr(text).to_string_lossy().into_owned(); - match tts.as_mut().unwrap().speak(text, interrupt) { - Ok(u) => { - if !utterance.is_null() { - *utterance = match u { - Some(u) => Box::into_raw(Box::new(u)), - None => ptr::null_mut(), - }; - } - return true; - } - Err(e) => { - set_last_error(e.to_string()).unwrap(); - return false; + let text = CStr::from_ptr(text).to_string_lossy().into_owned(); + match tts.as_mut().unwrap().speak(text, interrupt) { + Ok(u) => { + if !utterance.is_null() { + *utterance = match u { + Some(u) => Box::into_raw(Box::new(u)), + None => ptr::null_mut(), + }; } + return true; + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + return false; } } } @@ -122,22 +118,20 @@ pub extern "C" fn tts_speak( /// Free the memory associated with an `UtteranceId`. /// Does nothing if `utterance` is NULL. #[no_mangle] -pub extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { +pub unsafe extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { if utterance.is_null() { return; } - unsafe { - Box::from_raw(utterance); - } + Box::from_raw(utterance); } /// Stops current speech. /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] -pub extern "C" fn tts_stop(tts: *mut TTS) -> bool { +pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { if tts.is_null() { return false; } - match unsafe { tts.as_mut().unwrap().stop() } { + match tts.as_mut().unwrap().stop() { Ok(_) => true, Err(e) => { set_last_error(e.to_string()).unwrap(); From f4c7adfe5a1a061548fd830227057a316b3774d5 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:04:48 +0000 Subject: [PATCH 11/44] FFI: Allow getting and setting rate, and getting min, normal and max rate. --- src/ffi.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/ffi.rs b/src/ffi.rs index bf44363..542c7e0 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,4 +1,4 @@ -use libc::c_char; +use libc::{c_char, c_float}; use std::{ cell::RefCell, ffi::{CStr, CString, NulError}, @@ -139,3 +139,64 @@ pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { } } } + +/// Returns the minimum rate for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_rate(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().min_rate() +} + +/// Returns the maximum rate for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_rate(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().max_rate() +} + +/// Returns the normal rate for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_rate(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().normal_rate() +} + +/// Gets the current speech rate. +/// Returns true on success, false on error (likely that the backend doesn't support rate changes) +/// or if `tts` is NULL. +/// Does nothing if `rate` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_rate(tts: *mut TTS, rate: *mut c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_rate() { + Ok(r) => { + if !rate.is_null() { + *rate = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech rate. +/// Returns true on success, false on error (likely that the backend doesn't support rate changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_rate(rate) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From aa47b12622cef3798a50040b8424d7f359633708 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:20:22 +0000 Subject: [PATCH 12/44] FFI: Generate C bindings by default, not C++ More portible, and the FFI is much more C-like. --- cbindgen.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cbindgen.toml b/cbindgen.toml index 7b647ff..533cc46 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -1,3 +1,4 @@ +language = "c" include_guard = "TTS_RS_H" autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" From 73210d657264cb25a5106822168407c984c37ed8 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:29:34 +0000 Subject: [PATCH 13/44] FFI: Allow getting and setting pitch, and getting min, normal and max pitch. --- src/ffi.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index 542c7e0..ec16519 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -200,3 +200,64 @@ pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { } } } + +/// Returns the minimum pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_pitch(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().min_pitch() +} + +/// Returns the maximum pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_pitch(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().max_pitch() +} + +/// Returns the normal pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_pitch(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().normal_pitch() +} + +/// Gets the current speech pitch. +/// Returns true on success, false on error (likely that the backend doesn't support pitch changes) +/// or if `tts` is NULL. +/// Does nothing if `pitch` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_pitch(tts: *mut TTS, pitch: *mut c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_pitch() { + Ok(r) => { + if !pitch.is_null() { + *pitch = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech pitch. +/// Returns true on success, false on error (likely that the backend doesn't support pitch changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_pitch(pitch) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From a2c13dd21f18d627b4337042daf340c14015aa98 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:31:30 +0000 Subject: [PATCH 14/44] FFI: Allow getting and setting volume, and getting min, normal and max volume. --- src/ffi.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index ec16519..b0fcd29 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -261,3 +261,64 @@ pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { } } } + +/// Returns the minimum volume for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_volume(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().min_volume() +} + +/// Returns the maximum volume for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_volume(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().max_volume() +} + +/// Returns the normal volume for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_volume(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().normal_volume() +} + +/// Gets the current speech volume. +/// Returns true on success, false on error (likely that the backend doesn't support volume changes) +/// or if `tts` is NULL. +/// Does nothing if `volume` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_volume(tts: *mut TTS, volume: *mut c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_volume() { + Ok(r) => { + if !volume.is_null() { + *volume = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech volume. +/// Returns true on success, false on error (likely that the backend doesn't support volume changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_volume(volume) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From f421dd9362f24fbf5bffe9803b206e9b38b9c422 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:44:44 +0000 Subject: [PATCH 15/44] FFI: Implement tts_is_speaking() function. --- src/ffi.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index b0fcd29..f5dbb48 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -322,3 +322,28 @@ pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: c_float) -> bool } } } + +/// fills `speaking` with a bool indicating whether this speech synthesizer is speaking. +/// Returns true on success, false on error (likely that the backend doesn't support speaking +/// status) or if `tts` is NULL. +/// If `speaking` is NULL, returns this value instead, meaning you can't tell the difference +/// between an error occuring and the synth not speaking. +#[no_mangle] +pub unsafe extern "C" fn tts_is_speaking(tts: *mut TTS, speaking: *mut bool) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().is_speaking() { + Ok(s) => { + if speaking.is_null() { + return s; + } + *speaking = s; + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From d6504a33e95dc8fb2979a0953201f71495310db9 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 21:16:55 +0000 Subject: [PATCH 16/44] FFI: Make cbindgen generate extern C block for C++, use qualified screaming snake case for enum variants. --- cbindgen.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cbindgen.toml b/cbindgen.toml index 533cc46..ddb27e5 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -1,7 +1,12 @@ language = "c" +cpp_compat = true include_guard = "TTS_RS_H" +header = "/* SPDXLicenseIdentifier: MIT */" autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" +[enum] +rename_variants = "QualifiedScreamingSnakeCase" + [defines] "target_os = linux" = "__linux__" "target_os = macos" = "__APPLE__" From 496d183fa4a8433522a44c5869a55029d677955a Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 21:37:24 +0000 Subject: [PATCH 17/44] FFI: Add module level doc comment. --- src/ffi.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index f5dbb48..6f14190 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,3 +1,5 @@ +//! Bindings to this library to allow it to be called from C/C++. + use libc::{c_char, c_float}; use std::{ cell::RefCell, From 0ca3100a97dc37ccc510942ce357e048f7983a57 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 00:23:36 +0000 Subject: [PATCH 18/44] FFI: Create ffi Cargo feature and include ffi module. --- Cargo.toml | 2 ++ src/ffi.rs | 0 src/lib.rs | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 src/ffi.rs diff --git a/Cargo.toml b/Cargo.toml index cb08133..96d49c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,12 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] use_tolk = ["tolk"] +ffi = ["libc"] [dependencies] dyn-clonable = "0.9" lazy_static = "1" +libc = {version = "0.2", optional = true} log = "0.4" thiserror = "1" diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs index a4b94f5..7673420 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ use objc::{class, msg_send, sel, sel_impl}; use thiserror::Error; mod backends; +#[cfg(feature = "ffi")] +pub mod ffi; #[derive(Clone, Copy, Debug)] pub enum Backends { From c5bbbfcfd71963c462fa29f14d777b726eb7f43e Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 01:13:40 +0000 Subject: [PATCH 19/44] FFI: Create error handling code and LAST_ERROR static Any errors reported will cause the C API functions to return an error value (NULL or -1). The caller can then use: * const char* tts_get_error() to get a pointer to a string describing the error * void tts_clear_error() to deallocate any currently stored error message. --- src/ffi.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index e69de29..5a2e286 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -0,0 +1,37 @@ +use libc::c_char; +use std::{ + cell::RefCell, + ffi::{CString, NulError}, + ptr, +}; + +thread_local! { + /// Stores the last reported error, so it can be retrieved at will from C + static LAST_ERROR: RefCell> = RefCell::new(None); +} + +fn set_last_error>>(err: E) -> Result<(), NulError> { + LAST_ERROR.with(|last| { + *last.borrow_mut() = Some(CString::new(err)?); + Ok(()) + }) +} + +/// Get the last reported error as a const C string (const char*) +/// This string will be valid until at least the next call to `tts_get_error`. +/// It is never called internally by the library. +#[no_mangle] +pub extern "C" fn tts_get_error() -> *const c_char { + LAST_ERROR.with(|err| match &*err.borrow() { + Some(e) => e.as_ptr(), + None => ptr::null(), + }) +} + +/// Deallocate the last reported error (if any). +#[no_mangle] +pub extern "C" fn tts_clear_error() { + LAST_ERROR.with(|err| { + *err.borrow_mut() = None; + }); +} From 5e7ab42f596a2c45c8f1e5502ff1130d4e11c497 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 01:40:13 +0000 Subject: [PATCH 20/44] FFI: Implement tts_default() constructor and tts_free() destructor * tts_default() allocates a new TTS struct via it's default() constructor, returning a pointer to it or NULL on error. * tts_free(tts) destroys the TTS pointed to by tts. If tts is NULL, this function does nothing. --- src/ffi.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index 5a2e286..71f14d3 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -5,6 +5,8 @@ use std::{ ptr, }; +use crate::TTS; + thread_local! { /// Stores the last reported error, so it can be retrieved at will from C static LAST_ERROR: RefCell> = RefCell::new(None); @@ -35,3 +37,29 @@ pub extern "C" fn tts_clear_error() { *err.borrow_mut() = None; }); } + +/// Creates a new TTS object with the default backend and returns a pointer to it. +/// If an error occured, a null pointer is returned, +/// Call `tts_get_error()` for more information about the specific error. +#[no_mangle] +pub extern "C" fn tts_default() -> *mut TTS { + match TTS::default() { + Ok(tts) => Box::into_raw(Box::new(tts)), + Err(e) => { + set_last_error(e.to_string()).unwrap(); + ptr::null_mut() + } + } +} + +/// Frees the memory associated with a TTS object. +/// If `tts` is a null pointer, this function does nothing. +#[no_mangle] +pub extern "C" fn tts_free(tts: *mut TTS) { + if tts.is_null() { + return; + } + unsafe { + Box::from_raw(tts); // Goes out of scope and is dropped + } +} From 8e86afb444ae5f01b1640310cc45585d97022ce2 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 16:32:53 +0000 Subject: [PATCH 21/44] FFI: Implement tts_new(backend) This required giving the Backends enum and Features struct a C representation. --- src/ffi.rs | 16 +++++++++++++++- src/lib.rs | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/ffi.rs b/src/ffi.rs index 71f14d3..16cac9e 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -5,7 +5,7 @@ use std::{ ptr, }; -use crate::TTS; +use crate::{Backends, TTS}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -38,6 +38,20 @@ pub extern "C" fn tts_clear_error() { }); } +/// Creates a new TTS object with the specified backend and returns a pointer to it. +/// If an error occured, a null pointer is returned, +/// Call `tts_get_error()` for more information about the specific error. +#[no_mangle] +pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { + match TTS::new(backend) { + Ok(tts) => Box::into_raw(Box::new(tts)), + Err(e) => { + set_last_error(e.to_string()).unwrap(); + ptr::null_mut() + } + } +} + /// Creates a new TTS object with the default backend and returns a pointer to it. /// If an error occured, a null pointer is returned, /// Call `tts_get_error()` for more information about the specific error. diff --git a/src/lib.rs b/src/lib.rs index 7673420..648be45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ mod backends; #[cfg(feature = "ffi")] pub mod ffi; +#[repr(C)] #[derive(Clone, Copy, Debug)] pub enum Backends { #[cfg(target_os = "linux")] @@ -82,6 +83,7 @@ unsafe impl Send for UtteranceId {} unsafe impl Sync for UtteranceId {} +#[repr(C)] pub struct Features { pub stop: bool, pub rate: bool, From 3b1994ab361a2ad47f8415033e7c34b130929e15 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 16:35:08 +0000 Subject: [PATCH 22/44] FFI: Add cbindgen config for generation of C/C++ headers. --- cbindgen.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 cbindgen.toml diff --git a/cbindgen.toml b/cbindgen.toml new file mode 100644 index 0000000..7b647ff --- /dev/null +++ b/cbindgen.toml @@ -0,0 +1,8 @@ +include_guard = "TTS_RS_H" +autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" + +[defines] +"target_os = linux" = "__linux__" +"target_os = macos" = "__APPLE__" +"target_arch = wasm32" = "__EMSCRIPTEN__" +windows = "__WIN32__" From f6546303f8106cf17d8220fd93151a4eaede835a Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 16:59:07 +0000 Subject: [PATCH 23/44] FFI: Allow determining TTS supported Features from C/C++ --- src/ffi.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ffi.rs b/src/ffi.rs index 16cac9e..693d9f3 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -5,7 +5,7 @@ use std::{ ptr, }; -use crate::{Backends, TTS}; +use crate::{Backends, Features, TTS}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -77,3 +77,10 @@ pub extern "C" fn tts_free(tts: *mut TTS) { Box::from_raw(tts); // Goes out of scope and is dropped } } + +/// Returns the features supported by this TTS engine. +/// tts must be a valid pointer to a TTS object. +#[no_mangle] +pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { + unsafe { tts.as_ref().unwrap().supported_features() } +} From 53f352e1a8059f0d811c91be6f799d9d5a762a22 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 17:06:58 +0000 Subject: [PATCH 24/44] FFI: Update documentation comments to match their Rust equivilents. --- src/ffi.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 693d9f3..bb71589 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -38,8 +38,8 @@ pub extern "C" fn tts_clear_error() { }); } -/// Creates a new TTS object with the specified backend and returns a pointer to it. -/// If an error occured, a null pointer is returned, +/// Create a new `TTS` instance with the specified backend. +/// If an error occures, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { @@ -52,8 +52,8 @@ pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { } } -/// Creates a new TTS object with the default backend and returns a pointer to it. -/// If an error occured, a null pointer is returned, +/// Create a new TTS object with the default backend. +/// If an error occures, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] pub extern "C" fn tts_default() -> *mut TTS { @@ -66,7 +66,7 @@ pub extern "C" fn tts_default() -> *mut TTS { } } -/// Frees the memory associated with a TTS object. +/// Free the memory associated with a TTS object. /// If `tts` is a null pointer, this function does nothing. #[no_mangle] pub extern "C" fn tts_free(tts: *mut TTS) { @@ -79,7 +79,7 @@ pub extern "C" fn tts_free(tts: *mut TTS) { } /// Returns the features supported by this TTS engine. -/// tts must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a TTS object. #[no_mangle] pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { unsafe { tts.as_ref().unwrap().supported_features() } From 05a065c6d347368be91a438ad744d8971830abf1 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 19:11:05 +0000 Subject: [PATCH 25/44] FFI: Implement tts_speak() The tts_speak() function has an additional parameter that, if not NULL, will be filled with a pointer to an UtteranceId. If this is specified, the caller must also call tts_free_utterance() to deallocate the UtteranceId when they're done with it. --- src/ffi.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index bb71589..ec349e7 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,11 +1,11 @@ use libc::c_char; use std::{ cell::RefCell, - ffi::{CString, NulError}, + ffi::{CStr, CString, NulError}, ptr, }; -use crate::{Backends, Features, TTS}; +use crate::{Backends, Features, UtteranceId, TTS}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -19,7 +19,7 @@ fn set_last_error>>(err: E) -> Result<(), NulError> { }) } -/// Get the last reported error as a const C string (const char*) +/// Get the last reported error as a const C string. /// This string will be valid until at least the next call to `tts_get_error`. /// It is never called internally by the library. #[no_mangle] @@ -84,3 +84,49 @@ pub extern "C" fn tts_free(tts: *mut TTS) { pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { unsafe { tts.as_ref().unwrap().supported_features() } } + +/// Speaks the specified text, optionally interrupting current speech. +/// If `utterance` is not NULL, , fills it with a pointer to the returned UtteranceId (or NULL if +/// the backend doesn't provide one). +/// Returns true on success, false on error or if `tts` is NULL. +#[no_mangle] +pub extern "C" fn tts_speak( + tts: *mut TTS, + text: *const c_char, + interrupt: bool, + utterance: *mut *mut UtteranceId, +) -> bool { + if tts.is_null() { + return true; + } + unsafe { + let text = CStr::from_ptr(text).to_string_lossy().into_owned(); + match tts.as_mut().unwrap().speak(text, interrupt) { + Ok(u) => { + if !utterance.is_null() { + *utterance = match u { + Some(u) => Box::into_raw(Box::new(u)), + None => ptr::null_mut(), + }; + } + return true; + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + return false; + } + } + } +} + +/// Free the memory associated with an `UtteranceId`. +/// Does nothing if `utterance` is NULL. +#[no_mangle] +pub extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { + if utterance.is_null() { + return; + } + unsafe { + Box::from_raw(utterance); + } +} From 0905f6d6c6c1bd9273d4b73deb8b4bbe1c0f2d22 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 19:25:36 +0000 Subject: [PATCH 26/44] FFI: Implement tts_stop() function. --- src/ffi.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index ec349e7..6323ab6 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -130,3 +130,18 @@ pub extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { Box::from_raw(utterance); } } +/// Stops current speech. +/// Returns true on success, false on error or if `tts` is NULL. +#[no_mangle] +pub extern "C" fn tts_stop(tts: *mut TTS) -> bool { + if tts.is_null() { + return false; + } + match unsafe { tts.as_mut().unwrap().stop() } { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From 729ece9a07b6d5cad588454b767bc67e35b1b329 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 19:37:44 +0000 Subject: [PATCH 27/44] FFI: Make all functions with unsafe blocks completely unsafe. This is better than using unsafe blocks inside the functions, as that tells the compiler that the unsafeness won't leak out of the block, which isn't true in this case as we're dealing with another unsafe language. --- src/ffi.rs | 50 ++++++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 6323ab6..bf44363 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -69,20 +69,18 @@ pub extern "C" fn tts_default() -> *mut TTS { /// Free the memory associated with a TTS object. /// If `tts` is a null pointer, this function does nothing. #[no_mangle] -pub extern "C" fn tts_free(tts: *mut TTS) { +pub unsafe extern "C" fn tts_free(tts: *mut TTS) { if tts.is_null() { return; } - unsafe { - Box::from_raw(tts); // Goes out of scope and is dropped - } + Box::from_raw(tts); // Goes out of scope and is dropped } /// Returns the features supported by this TTS engine. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { - unsafe { tts.as_ref().unwrap().supported_features() } +pub unsafe extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { + tts.as_ref().unwrap().supported_features() } /// Speaks the specified text, optionally interrupting current speech. @@ -90,7 +88,7 @@ pub extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { /// the backend doesn't provide one). /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] -pub extern "C" fn tts_speak( +pub unsafe extern "C" fn tts_speak( tts: *mut TTS, text: *const c_char, interrupt: bool, @@ -99,22 +97,20 @@ pub extern "C" fn tts_speak( if tts.is_null() { return true; } - unsafe { - let text = CStr::from_ptr(text).to_string_lossy().into_owned(); - match tts.as_mut().unwrap().speak(text, interrupt) { - Ok(u) => { - if !utterance.is_null() { - *utterance = match u { - Some(u) => Box::into_raw(Box::new(u)), - None => ptr::null_mut(), - }; - } - return true; - } - Err(e) => { - set_last_error(e.to_string()).unwrap(); - return false; + let text = CStr::from_ptr(text).to_string_lossy().into_owned(); + match tts.as_mut().unwrap().speak(text, interrupt) { + Ok(u) => { + if !utterance.is_null() { + *utterance = match u { + Some(u) => Box::into_raw(Box::new(u)), + None => ptr::null_mut(), + }; } + return true; + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + return false; } } } @@ -122,22 +118,20 @@ pub extern "C" fn tts_speak( /// Free the memory associated with an `UtteranceId`. /// Does nothing if `utterance` is NULL. #[no_mangle] -pub extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { +pub unsafe extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { if utterance.is_null() { return; } - unsafe { - Box::from_raw(utterance); - } + Box::from_raw(utterance); } /// Stops current speech. /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] -pub extern "C" fn tts_stop(tts: *mut TTS) -> bool { +pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { if tts.is_null() { return false; } - match unsafe { tts.as_mut().unwrap().stop() } { + match tts.as_mut().unwrap().stop() { Ok(_) => true, Err(e) => { set_last_error(e.to_string()).unwrap(); From cf35c19c578aa9cdce62aec1344a2a02ff0e47a1 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:04:48 +0000 Subject: [PATCH 28/44] FFI: Allow getting and setting rate, and getting min, normal and max rate. --- src/ffi.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/ffi.rs b/src/ffi.rs index bf44363..542c7e0 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,4 +1,4 @@ -use libc::c_char; +use libc::{c_char, c_float}; use std::{ cell::RefCell, ffi::{CStr, CString, NulError}, @@ -139,3 +139,64 @@ pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { } } } + +/// Returns the minimum rate for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_rate(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().min_rate() +} + +/// Returns the maximum rate for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_rate(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().max_rate() +} + +/// Returns the normal rate for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_rate(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().normal_rate() +} + +/// Gets the current speech rate. +/// Returns true on success, false on error (likely that the backend doesn't support rate changes) +/// or if `tts` is NULL. +/// Does nothing if `rate` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_rate(tts: *mut TTS, rate: *mut c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_rate() { + Ok(r) => { + if !rate.is_null() { + *rate = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech rate. +/// Returns true on success, false on error (likely that the backend doesn't support rate changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_rate(rate) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From 6df1fe7f0ea053ad91048dd32b2a1b8a893344ff Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:20:22 +0000 Subject: [PATCH 29/44] FFI: Generate C bindings by default, not C++ More portible, and the FFI is much more C-like. --- cbindgen.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/cbindgen.toml b/cbindgen.toml index 7b647ff..533cc46 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -1,3 +1,4 @@ +language = "c" include_guard = "TTS_RS_H" autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" From 32c9327f5c8c8241b3b08790ea710e39bce09857 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:29:34 +0000 Subject: [PATCH 30/44] FFI: Allow getting and setting pitch, and getting min, normal and max pitch. --- src/ffi.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index 542c7e0..ec16519 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -200,3 +200,64 @@ pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { } } } + +/// Returns the minimum pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_pitch(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().min_pitch() +} + +/// Returns the maximum pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_pitch(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().max_pitch() +} + +/// Returns the normal pitch for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_pitch(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().normal_pitch() +} + +/// Gets the current speech pitch. +/// Returns true on success, false on error (likely that the backend doesn't support pitch changes) +/// or if `tts` is NULL. +/// Does nothing if `pitch` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_pitch(tts: *mut TTS, pitch: *mut c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_pitch() { + Ok(r) => { + if !pitch.is_null() { + *pitch = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech pitch. +/// Returns true on success, false on error (likely that the backend doesn't support pitch changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_pitch(pitch) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From 0ffa4c6afe60955fe257d95e2b2d574b1db4d713 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:31:30 +0000 Subject: [PATCH 31/44] FFI: Allow getting and setting volume, and getting min, normal and max volume. --- src/ffi.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index ec16519..b0fcd29 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -261,3 +261,64 @@ pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { } } } + +/// Returns the minimum volume for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_min_volume(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().min_volume() +} + +/// Returns the maximum volume for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_max_volume(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().max_volume() +} + +/// Returns the normal volume for this speech synthesizer. +/// `tts` must be a valid pointer to a TTS object. +#[no_mangle] +pub unsafe extern "C" fn tts_normal_volume(tts: *mut TTS) -> c_float { + tts.as_ref().unwrap().normal_volume() +} + +/// Gets the current speech volume. +/// Returns true on success, false on error (likely that the backend doesn't support volume changes) +/// or if `tts` is NULL. +/// Does nothing if `volume` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_get_volume(tts: *mut TTS, volume: *mut c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().get_volume() { + Ok(r) => { + if !volume.is_null() { + *volume = r; + } + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} + +/// Sets the desired speech volume. +/// Returns true on success, false on error (likely that the backend doesn't support volume changes) +/// or if `tts` is NULL. +#[no_mangle] +pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: c_float) -> bool { + if tts.is_null() { + return false; + } + match tts.as_mut().unwrap().set_volume(volume) { + Ok(_) => true, + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From 989b6bb310dddb5a5668c71ac8235700f94c1375 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 20:44:44 +0000 Subject: [PATCH 32/44] FFI: Implement tts_is_speaking() function. --- src/ffi.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index b0fcd29..f5dbb48 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -322,3 +322,28 @@ pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: c_float) -> bool } } } + +/// fills `speaking` with a bool indicating whether this speech synthesizer is speaking. +/// Returns true on success, false on error (likely that the backend doesn't support speaking +/// status) or if `tts` is NULL. +/// If `speaking` is NULL, returns this value instead, meaning you can't tell the difference +/// between an error occuring and the synth not speaking. +#[no_mangle] +pub unsafe extern "C" fn tts_is_speaking(tts: *mut TTS, speaking: *mut bool) -> bool { + if tts.is_null() { + return false; + } + match tts.as_ref().unwrap().is_speaking() { + Ok(s) => { + if speaking.is_null() { + return s; + } + *speaking = s; + true + } + Err(e) => { + set_last_error(e.to_string()).unwrap(); + false + } + } +} From 1a49a8dac11803f5c8411a1dd9a9ba9bd23f79c2 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 21:16:55 +0000 Subject: [PATCH 33/44] FFI: Make cbindgen generate extern C block for C++, use qualified screaming snake case for enum variants. --- cbindgen.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cbindgen.toml b/cbindgen.toml index 533cc46..ddb27e5 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -1,7 +1,12 @@ language = "c" +cpp_compat = true include_guard = "TTS_RS_H" +header = "/* SPDXLicenseIdentifier: MIT */" autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" +[enum] +rename_variants = "QualifiedScreamingSnakeCase" + [defines] "target_os = linux" = "__linux__" "target_os = macos" = "__APPLE__" From a868aa670a2a4b231b51276f22b8c82235a36332 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 12 Dec 2020 21:37:24 +0000 Subject: [PATCH 34/44] FFI: Add module level doc comment. --- src/ffi.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ffi.rs b/src/ffi.rs index f5dbb48..6f14190 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,3 +1,5 @@ +//! Bindings to this library to allow it to be called from C/C++. + use libc::{c_char, c_float}; use std::{ cell::RefCell, From af551018242100ab401a838e58af8ef07c1ac232 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Thu, 31 Dec 2020 15:13:13 +0000 Subject: [PATCH 35/44] FFI: Add #ifdef for Android and don't generate declarations for Java bindings. --- cbindgen.toml | 1 + src/backends/mod.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/cbindgen.toml b/cbindgen.toml index ddb27e5..f31ddbe 100644 --- a/cbindgen.toml +++ b/cbindgen.toml @@ -8,6 +8,7 @@ autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't mod rename_variants = "QualifiedScreamingSnakeCase" [defines] +"target_os = android" = "__ANDROID__" "target_os = linux" = "__linux__" "target_os = macos" = "__APPLE__" "target_arch = wasm32" = "__EMSCRIPTEN__" diff --git a/src/backends/mod.rs b/src/backends/mod.rs index c36c233..a40ca57 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -16,6 +16,7 @@ mod appkit; #[cfg(any(target_os = "macos", target_os = "ios"))] mod av_foundation; +/// cbindgen:ignore #[cfg(target_os = "android")] mod android; From 078cf83a1339e87bedad91bc9be28eeb1513b8e1 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Thu, 31 Dec 2020 15:28:03 +0000 Subject: [PATCH 36/44] FFI::: Add gen-c-bindings cargo-make target This currently always saves the bindings to target/release. --- Makefile.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Makefile.toml b/Makefile.toml index 6697f15..241094f 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,3 +1,8 @@ +[tasks.gen-c-bindings] +install_crate = "cbindgen" +command = "cbindgen" +args = ["-c", "cbindgen.toml", "-o", "target/release/tts-rs.h"] + [tasks.build-android-example] script = [ "cd examples/android", @@ -12,4 +17,4 @@ script = [ [tasks.log-android] command = "adb" -args = ["logcat", "RustStdoutStderr:D", "*:S"] \ No newline at end of file +args = ["logcat", "RustStdoutStderr:D", "*:S"] From bab977c0ea521764ac61bddc9a1fb1d969218e83 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Thu, 31 Dec 2020 15:31:46 +0000 Subject: [PATCH 37/44] FFI:: Fix spelling error in doc comments. --- src/ffi.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 6f14190..a8dd212 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -41,7 +41,7 @@ pub extern "C" fn tts_clear_error() { } /// Create a new `TTS` instance with the specified backend. -/// If an error occures, returns a null pointer, +/// If an error occurs, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { @@ -55,7 +55,7 @@ pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { } /// Create a new TTS object with the default backend. -/// If an error occures, returns a null pointer, +/// If an error occurs, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] pub extern "C" fn tts_default() -> *mut TTS { From 060a057c0f92d46602f40999822a2670c67945b4 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 6 Mar 2021 09:36:45 +0000 Subject: [PATCH 38/44] FFI: Take only const pointers to TTS objects when we don't modify them. --- src/ffi.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index a8dd212..077517c 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -81,7 +81,7 @@ pub unsafe extern "C" fn tts_free(tts: *mut TTS) { /// Returns the features supported by this TTS engine. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_supported_features(tts: *mut TTS) -> Features { +pub unsafe extern "C" fn tts_supported_features(tts: *const TTS) -> Features { tts.as_ref().unwrap().supported_features() } @@ -97,7 +97,7 @@ pub unsafe extern "C" fn tts_speak( utterance: *mut *mut UtteranceId, ) -> bool { if tts.is_null() { - return true; + return false; } let text = CStr::from_ptr(text).to_string_lossy().into_owned(); match tts.as_mut().unwrap().speak(text, interrupt) { @@ -126,6 +126,7 @@ pub unsafe extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { } Box::from_raw(utterance); } + /// Stops current speech. /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] @@ -145,21 +146,21 @@ pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { /// Returns the minimum rate for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_min_rate(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_min_rate(tts: *const TTS) -> c_float { tts.as_ref().unwrap().min_rate() } /// Returns the maximum rate for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_max_rate(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_max_rate(tts: *const TTS) -> c_float { tts.as_ref().unwrap().max_rate() } /// Returns the normal rate for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_rate(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_normal_rate(tts: *const TTS) -> c_float { tts.as_ref().unwrap().normal_rate() } @@ -168,7 +169,7 @@ pub unsafe extern "C" fn tts_normal_rate(tts: *mut TTS) -> c_float { /// or if `tts` is NULL. /// Does nothing if `rate` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_rate(tts: *mut TTS, rate: *mut c_float) -> bool { +pub unsafe extern "C" fn tts_get_rate(tts: *const TTS, rate: *mut c_float) -> bool { if tts.is_null() { return false; } @@ -206,21 +207,21 @@ pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { /// Returns the minimum pitch for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_min_pitch(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_min_pitch(tts: *const TTS) -> c_float { tts.as_ref().unwrap().min_pitch() } /// Returns the maximum pitch for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_max_pitch(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_max_pitch(tts: *const TTS) -> c_float { tts.as_ref().unwrap().max_pitch() } /// Returns the normal pitch for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_pitch(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_normal_pitch(tts: *const TTS) -> c_float { tts.as_ref().unwrap().normal_pitch() } @@ -229,7 +230,7 @@ pub unsafe extern "C" fn tts_normal_pitch(tts: *mut TTS) -> c_float { /// or if `tts` is NULL. /// Does nothing if `pitch` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_pitch(tts: *mut TTS, pitch: *mut c_float) -> bool { +pub unsafe extern "C" fn tts_get_pitch(tts: *const TTS, pitch: *mut c_float) -> bool { if tts.is_null() { return false; } @@ -267,21 +268,21 @@ pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { /// Returns the minimum volume for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_min_volume(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_min_volume(tts: *const TTS) -> c_float { tts.as_ref().unwrap().min_volume() } /// Returns the maximum volume for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_max_volume(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_max_volume(tts: *const TTS) -> c_float { tts.as_ref().unwrap().max_volume() } /// Returns the normal volume for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_volume(tts: *mut TTS) -> c_float { +pub unsafe extern "C" fn tts_normal_volume(tts: *const TTS) -> c_float { tts.as_ref().unwrap().normal_volume() } @@ -290,7 +291,7 @@ pub unsafe extern "C" fn tts_normal_volume(tts: *mut TTS) -> c_float { /// or if `tts` is NULL. /// Does nothing if `volume` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_volume(tts: *mut TTS, volume: *mut c_float) -> bool { +pub unsafe extern "C" fn tts_get_volume(tts: *const TTS, volume: *mut c_float) -> bool { if tts.is_null() { return false; } @@ -331,7 +332,7 @@ pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: c_float) -> bool /// If `speaking` is NULL, returns this value instead, meaning you can't tell the difference /// between an error occuring and the synth not speaking. #[no_mangle] -pub unsafe extern "C" fn tts_is_speaking(tts: *mut TTS, speaking: *mut bool) -> bool { +pub unsafe extern "C" fn tts_is_speaking(tts: *const TTS, speaking: *mut bool) -> bool { if tts.is_null() { return false; } From 0644f03f34e3e6cf803bd474349acfd7447ce0ac Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 6 Mar 2021 09:56:57 +0000 Subject: [PATCH 39/44] FFI: Automatically generate bindings when building with ffi feature. --- Cargo.toml | 3 +++ build.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7b8c389..3df1d26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,9 @@ thiserror = "1" [dev-dependencies] env_logger = "0.8" +[build-dependencies] +cbindgen = "0.18.0" + [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } windows = "0.2" diff --git a/build.rs b/build.rs index 8b1edc2..c92ffc7 100644 --- a/build.rs +++ b/build.rs @@ -8,4 +8,18 @@ fn main() { println!("cargo:rustc-link-lib=framework=AppKit"); } } + + #[cfg(feature = "ffi")] + generate_c_bindings(); +} + +#[cfg(feature = "ffi")] +fn generate_c_bindings() { + use std::path::PathBuf; + let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + let mut header_path: PathBuf = std::env::var("OUT_DIR").unwrap().into(); + header_path.push("tts.h"); + cbindgen::generate(crate_dir) + .unwrap() + .write_to_file(header_path); } From b972f44bc9aa7b5d83c534ebd47636c8ab0d8f04 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 6 Mar 2021 11:30:42 +0000 Subject: [PATCH 40/44] FFI: Don't use libc for C types like char and float This insures the correct C standards are followed, but doesn't work on wasm32-unknown-unknown targets, because there *is* no libc. Given that the definition of `char` and `float` are very universal anyway, it makes sense to just use `i8` and `f32`. --- Cargo.toml | 2 +- src/ffi.rs | 35 +++++++++++++++++------------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3df1d26..c56a793 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] use_tolk = ["tolk"] -ffi = ["libc"] +ffi = [] [dependencies] dyn-clonable = "0.9" diff --git a/src/ffi.rs b/src/ffi.rs index 077517c..0fb65ec 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -1,6 +1,5 @@ //! Bindings to this library to allow it to be called from C/C++. -use libc::{c_char, c_float}; use std::{ cell::RefCell, ffi::{CStr, CString, NulError}, @@ -25,7 +24,7 @@ fn set_last_error>>(err: E) -> Result<(), NulError> { /// This string will be valid until at least the next call to `tts_get_error`. /// It is never called internally by the library. #[no_mangle] -pub extern "C" fn tts_get_error() -> *const c_char { +pub extern "C" fn tts_get_error() -> *const i8 { LAST_ERROR.with(|err| match &*err.borrow() { Some(e) => e.as_ptr(), None => ptr::null(), @@ -92,7 +91,7 @@ pub unsafe extern "C" fn tts_supported_features(tts: *const TTS) -> Features { #[no_mangle] pub unsafe extern "C" fn tts_speak( tts: *mut TTS, - text: *const c_char, + text: *const i8, interrupt: bool, utterance: *mut *mut UtteranceId, ) -> bool { @@ -146,21 +145,21 @@ pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { /// Returns the minimum rate for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_min_rate(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_min_rate(tts: *const TTS) -> f32 { tts.as_ref().unwrap().min_rate() } /// Returns the maximum rate for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_max_rate(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_max_rate(tts: *const TTS) -> f32 { tts.as_ref().unwrap().max_rate() } /// Returns the normal rate for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_rate(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_normal_rate(tts: *const TTS) -> f32 { tts.as_ref().unwrap().normal_rate() } @@ -169,7 +168,7 @@ pub unsafe extern "C" fn tts_normal_rate(tts: *const TTS) -> c_float { /// or if `tts` is NULL. /// Does nothing if `rate` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_rate(tts: *const TTS, rate: *mut c_float) -> bool { +pub unsafe extern "C" fn tts_get_rate(tts: *const TTS, rate: *mut f32) -> bool { if tts.is_null() { return false; } @@ -191,7 +190,7 @@ pub unsafe extern "C" fn tts_get_rate(tts: *const TTS, rate: *mut c_float) -> bo /// Returns true on success, false on error (likely that the backend doesn't support rate changes) /// or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { +pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: f32) -> bool { if tts.is_null() { return false; } @@ -207,21 +206,21 @@ pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: c_float) -> bool { /// Returns the minimum pitch for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_min_pitch(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_min_pitch(tts: *const TTS) -> f32 { tts.as_ref().unwrap().min_pitch() } /// Returns the maximum pitch for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_max_pitch(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_max_pitch(tts: *const TTS) -> f32 { tts.as_ref().unwrap().max_pitch() } /// Returns the normal pitch for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_pitch(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_normal_pitch(tts: *const TTS) -> f32 { tts.as_ref().unwrap().normal_pitch() } @@ -230,7 +229,7 @@ pub unsafe extern "C" fn tts_normal_pitch(tts: *const TTS) -> c_float { /// or if `tts` is NULL. /// Does nothing if `pitch` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_pitch(tts: *const TTS, pitch: *mut c_float) -> bool { +pub unsafe extern "C" fn tts_get_pitch(tts: *const TTS, pitch: *mut f32) -> bool { if tts.is_null() { return false; } @@ -252,7 +251,7 @@ pub unsafe extern "C" fn tts_get_pitch(tts: *const TTS, pitch: *mut c_float) -> /// Returns true on success, false on error (likely that the backend doesn't support pitch changes) /// or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { +pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: f32) -> bool { if tts.is_null() { return false; } @@ -268,21 +267,21 @@ pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: c_float) -> bool { /// Returns the minimum volume for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_min_volume(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_min_volume(tts: *const TTS) -> f32 { tts.as_ref().unwrap().min_volume() } /// Returns the maximum volume for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_max_volume(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_max_volume(tts: *const TTS) -> f32 { tts.as_ref().unwrap().max_volume() } /// Returns the normal volume for this speech synthesizer. /// `tts` must be a valid pointer to a TTS object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_volume(tts: *const TTS) -> c_float { +pub unsafe extern "C" fn tts_normal_volume(tts: *const TTS) -> f32 { tts.as_ref().unwrap().normal_volume() } @@ -291,7 +290,7 @@ pub unsafe extern "C" fn tts_normal_volume(tts: *const TTS) -> c_float { /// or if `tts` is NULL. /// Does nothing if `volume` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_volume(tts: *const TTS, volume: *mut c_float) -> bool { +pub unsafe extern "C" fn tts_get_volume(tts: *const TTS, volume: *mut f32) -> bool { if tts.is_null() { return false; } @@ -313,7 +312,7 @@ pub unsafe extern "C" fn tts_get_volume(tts: *const TTS, volume: *mut c_float) - /// Returns true on success, false on error (likely that the backend doesn't support volume changes) /// or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: c_float) -> bool { +pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: f32) -> bool { if tts.is_null() { return false; } From 6b726463cd5fcc62ba4f805cee797a694ab6bc53 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Sat, 6 Mar 2021 13:08:31 +0000 Subject: [PATCH 41/44] FFI: Use std::os::raw::c_char instead of i8 or libc's c_char This fixes an issue where the string pointer type in the generated bindings was const int8_t *. This works the same way, but could be confusing. --- src/ffi.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 0fb65ec..717f8e1 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -3,6 +3,7 @@ use std::{ cell::RefCell, ffi::{CStr, CString, NulError}, + os::raw::c_char, ptr, }; @@ -24,7 +25,7 @@ fn set_last_error>>(err: E) -> Result<(), NulError> { /// This string will be valid until at least the next call to `tts_get_error`. /// It is never called internally by the library. #[no_mangle] -pub extern "C" fn tts_get_error() -> *const i8 { +pub extern "C" fn tts_get_error() -> *const c_char { LAST_ERROR.with(|err| match &*err.borrow() { Some(e) => e.as_ptr(), None => ptr::null(), @@ -91,7 +92,7 @@ pub unsafe extern "C" fn tts_supported_features(tts: *const TTS) -> Features { #[no_mangle] pub unsafe extern "C" fn tts_speak( tts: *mut TTS, - text: *const i8, + text: *const c_char, interrupt: bool, utterance: *mut *mut UtteranceId, ) -> bool { From a8f3cff0b8ea0c1c3a84fc8008c4a599a860a509 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Tue, 28 Sep 2021 09:18:56 +0100 Subject: [PATCH 42/44] FFI: Make cbindgen an optional dependency, enabled with the ffi feature. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c56a793..d58d3b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["lib", "cdylib", "staticlib"] [features] use_tolk = ["tolk"] -ffi = [] +ffi = ["cbindgen"] [dependencies] dyn-clonable = "0.9" @@ -26,7 +26,7 @@ thiserror = "1" env_logger = "0.8" [build-dependencies] -cbindgen = "0.18.0" +cbindgen = {version = "0.18.0", optional = true} [target.'cfg(windows)'.dependencies] tolk = { version = "0.3", optional = true } From 6155937f06e6192bb90da9151dc91ae48af86841 Mon Sep 17 00:00:00 2001 From: mcb2003 Date: Tue, 28 Sep 2021 09:38:20 +0100 Subject: [PATCH 43/44] FFI: Use rename crate::Tts instead of crate::TTS. --- src/ffi.rs | 78 +++++++++++++++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 717f8e1..7986cb4 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -7,7 +7,7 @@ use std::{ ptr, }; -use crate::{Backends, Features, UtteranceId, TTS}; +use crate::{Backends, Features, UtteranceId, Tts}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -40,12 +40,12 @@ pub extern "C" fn tts_clear_error() { }); } -/// Create a new `TTS` instance with the specified backend. +/// Create a new `Tts` instance with the specified backend. /// If an error occurs, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] -pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { - match TTS::new(backend) { +pub extern "C" fn tts_new(backend: Backends) -> *mut Tts { + match Tts::new(backend) { Ok(tts) => Box::into_raw(Box::new(tts)), Err(e) => { set_last_error(e.to_string()).unwrap(); @@ -54,12 +54,12 @@ pub extern "C" fn tts_new(backend: Backends) -> *mut TTS { } } -/// Create a new TTS object with the default backend. +/// Create a new Tts object with the default backend. /// If an error occurs, returns a null pointer, /// Call `tts_get_error()` for more information about the specific error. #[no_mangle] -pub extern "C" fn tts_default() -> *mut TTS { - match TTS::default() { +pub extern "C" fn tts_default() -> *mut Tts { + match Tts::default() { Ok(tts) => Box::into_raw(Box::new(tts)), Err(e) => { set_last_error(e.to_string()).unwrap(); @@ -68,20 +68,20 @@ pub extern "C" fn tts_default() -> *mut TTS { } } -/// Free the memory associated with a TTS object. +/// Free the memory associated with a Tts object. /// If `tts` is a null pointer, this function does nothing. #[no_mangle] -pub unsafe extern "C" fn tts_free(tts: *mut TTS) { +pub unsafe extern "C" fn tts_free(tts: *mut Tts) { if tts.is_null() { return; } Box::from_raw(tts); // Goes out of scope and is dropped } -/// Returns the features supported by this TTS engine. -/// `tts` must be a valid pointer to a TTS object. +/// Returns the features supported by this Tts engine. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_supported_features(tts: *const TTS) -> Features { +pub unsafe extern "C" fn tts_supported_features(tts: *const Tts) -> Features { tts.as_ref().unwrap().supported_features() } @@ -91,7 +91,7 @@ pub unsafe extern "C" fn tts_supported_features(tts: *const TTS) -> Features { /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] pub unsafe extern "C" fn tts_speak( - tts: *mut TTS, + tts: *mut Tts, text: *const c_char, interrupt: bool, utterance: *mut *mut UtteranceId, @@ -130,7 +130,7 @@ pub unsafe extern "C" fn tts_free_utterance(utterance: *mut UtteranceId) { /// Stops current speech. /// Returns true on success, false on error or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { +pub unsafe extern "C" fn tts_stop(tts: *mut Tts) -> bool { if tts.is_null() { return false; } @@ -144,23 +144,23 @@ pub unsafe extern "C" fn tts_stop(tts: *mut TTS) -> bool { } /// Returns the minimum rate for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_min_rate(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_min_rate(tts: *const Tts) -> f32 { tts.as_ref().unwrap().min_rate() } /// Returns the maximum rate for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_max_rate(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_max_rate(tts: *const Tts) -> f32 { tts.as_ref().unwrap().max_rate() } /// Returns the normal rate for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_rate(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_normal_rate(tts: *const Tts) -> f32 { tts.as_ref().unwrap().normal_rate() } @@ -169,7 +169,7 @@ pub unsafe extern "C" fn tts_normal_rate(tts: *const TTS) -> f32 { /// or if `tts` is NULL. /// Does nothing if `rate` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_rate(tts: *const TTS, rate: *mut f32) -> bool { +pub unsafe extern "C" fn tts_get_rate(tts: *const Tts, rate: *mut f32) -> bool { if tts.is_null() { return false; } @@ -191,7 +191,7 @@ pub unsafe extern "C" fn tts_get_rate(tts: *const TTS, rate: *mut f32) -> bool { /// Returns true on success, false on error (likely that the backend doesn't support rate changes) /// or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: f32) -> bool { +pub unsafe extern "C" fn tts_set_rate(tts: *mut Tts, rate: f32) -> bool { if tts.is_null() { return false; } @@ -205,23 +205,23 @@ pub unsafe extern "C" fn tts_set_rate(tts: *mut TTS, rate: f32) -> bool { } /// Returns the minimum pitch for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_min_pitch(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_min_pitch(tts: *const Tts) -> f32 { tts.as_ref().unwrap().min_pitch() } /// Returns the maximum pitch for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_max_pitch(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_max_pitch(tts: *const Tts) -> f32 { tts.as_ref().unwrap().max_pitch() } /// Returns the normal pitch for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_pitch(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_normal_pitch(tts: *const Tts) -> f32 { tts.as_ref().unwrap().normal_pitch() } @@ -230,7 +230,7 @@ pub unsafe extern "C" fn tts_normal_pitch(tts: *const TTS) -> f32 { /// or if `tts` is NULL. /// Does nothing if `pitch` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_pitch(tts: *const TTS, pitch: *mut f32) -> bool { +pub unsafe extern "C" fn tts_get_pitch(tts: *const Tts, pitch: *mut f32) -> bool { if tts.is_null() { return false; } @@ -252,7 +252,7 @@ pub unsafe extern "C" fn tts_get_pitch(tts: *const TTS, pitch: *mut f32) -> bool /// Returns true on success, false on error (likely that the backend doesn't support pitch changes) /// or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: f32) -> bool { +pub unsafe extern "C" fn tts_set_pitch(tts: *mut Tts, pitch: f32) -> bool { if tts.is_null() { return false; } @@ -266,23 +266,23 @@ pub unsafe extern "C" fn tts_set_pitch(tts: *mut TTS, pitch: f32) -> bool { } /// Returns the minimum volume for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_min_volume(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_min_volume(tts: *const Tts) -> f32 { tts.as_ref().unwrap().min_volume() } /// Returns the maximum volume for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_max_volume(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_max_volume(tts: *const Tts) -> f32 { tts.as_ref().unwrap().max_volume() } /// Returns the normal volume for this speech synthesizer. -/// `tts` must be a valid pointer to a TTS object. +/// `tts` must be a valid pointer to a Tts object. #[no_mangle] -pub unsafe extern "C" fn tts_normal_volume(tts: *const TTS) -> f32 { +pub unsafe extern "C" fn tts_normal_volume(tts: *const Tts) -> f32 { tts.as_ref().unwrap().normal_volume() } @@ -291,7 +291,7 @@ pub unsafe extern "C" fn tts_normal_volume(tts: *const TTS) -> f32 { /// or if `tts` is NULL. /// Does nothing if `volume` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_get_volume(tts: *const TTS, volume: *mut f32) -> bool { +pub unsafe extern "C" fn tts_get_volume(tts: *const Tts, volume: *mut f32) -> bool { if tts.is_null() { return false; } @@ -313,7 +313,7 @@ pub unsafe extern "C" fn tts_get_volume(tts: *const TTS, volume: *mut f32) -> bo /// Returns true on success, false on error (likely that the backend doesn't support volume changes) /// or if `tts` is NULL. #[no_mangle] -pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: f32) -> bool { +pub unsafe extern "C" fn tts_set_volume(tts: *mut Tts, volume: f32) -> bool { if tts.is_null() { return false; } @@ -332,7 +332,7 @@ pub unsafe extern "C" fn tts_set_volume(tts: *mut TTS, volume: f32) -> bool { /// If `speaking` is NULL, returns this value instead, meaning you can't tell the difference /// between an error occuring and the synth not speaking. #[no_mangle] -pub unsafe extern "C" fn tts_is_speaking(tts: *const TTS, speaking: *mut bool) -> bool { +pub unsafe extern "C" fn tts_is_speaking(tts: *const Tts, speaking: *mut bool) -> bool { if tts.is_null() { return false; } From f39f86aa13036e9f443c61ccd191795a7b5b3def Mon Sep 17 00:00:00 2001 From: Michael Connor Buchan Date: Sat, 23 Jul 2022 13:41:39 +0100 Subject: [PATCH 44/44] FFI: Pass pointer to uninit Features struct in tts_supported_features() This is done because the ABI for returning struct values isn't well defined. --- src/ffi.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 7986cb4..b89cd71 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -7,7 +7,7 @@ use std::{ ptr, }; -use crate::{Backends, Features, UtteranceId, Tts}; +use crate::{Backends, Features, Tts, UtteranceId}; thread_local! { /// Stores the last reported error, so it can be retrieved at will from C @@ -78,11 +78,12 @@ pub unsafe extern "C" fn tts_free(tts: *mut Tts) { Box::from_raw(tts); // Goes out of scope and is dropped } -/// Returns the features supported by this Tts engine. +/// Returns the features supported by this Tts engine in the object specified by `features`. /// `tts` must be a valid pointer to a Tts object. +/// `features` must be a valid pointer to an uninitialized `Features` object. #[no_mangle] -pub unsafe extern "C" fn tts_supported_features(tts: *const Tts) -> Features { - tts.as_ref().unwrap().supported_features() +pub unsafe extern "C" fn tts_supported_features(tts: *const Tts, features: *mut Features) { + *features = tts.as_ref().unwrap().supported_features() } /// Speaks the specified text, optionally interrupting current speech.