Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add C FFI Bindings #4

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open

Add C FFI Bindings #4

wants to merge 48 commits into from

Conversation

mcb2003
Copy link

@mcb2003 mcb2003 commented Dec 12, 2020

This PR adds extern "C" bindings to all functionality of the TTS struct, plus that of Features and Backends. The bindings are gated behind a new "ffi" Cargo feature.

Error Handling

Since we don't have the luxury of Result<T, E> in C, I created a thread-local static to store the last error in string form, and two functions:

  • tts_get_error() to retrieve any errors.
  • tts_clear_error() to deallocate the last error (if any). This is probably unlikely to get used that often, since Rust's CString will deallocate itself when dropped.

The caller can then determine if an error has occurred by examining the return value of most functions (if it is NULL, or false, something went wrong).

Generating the Bindings

I used cbindgen to create the C headers in testing, so included is a cbindgen.toml with some relatively sane settings.

One problem is detecting when we're compiling for iOS. I haven't found any way to do this that can be explained to cbindgen, so the headers would have to be modified manually. Consequently, the Apple section of the Backends enum currently looks like this:

#if defined(__APPLE__)
  BACKENDS_APP_KIT,
#endif
#if defined(__APPLE__)
  BACKENDS_AV_FOUNDATION,
#endif

Limitations

  • Allowing Rust to call C callback functions to notify on utterance start, end and stop seems pretty complex, namely because the on_utterance_* functions take a Boxed FnMut wide pointer.
  • When the static last_error CString is updated, a CString is constructed from a Rust String. Since there's no good way to losslessly get rid of '\0' bytes in the middle of a Rust String when converting to CString that I know of, if any error messages contain null bytes, Rust will panick and the result will be UB.

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.
* 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.
This required giving the Backends enum and Features struct a C
representation.
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.
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.
More portible, and the FFI is much more C-like.
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.
* 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.
This required giving the Backends enum and Features struct a C
representation.
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.
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.
More portible, and the FFI is much more C-like.
@mcb2003
Copy link
Author

mcb2003 commented Mar 6, 2021

I've now modified build.rs to automatically generate the C bindings and save them to $OUT_DIR/tts.h.

I've also fixed the failing wasm test. This was caused by libc not defining c_float and c_char for wasm32-unknown-unknown targets because, well, there is no libc for that target. See commit b972f44 and this libc crate issue for more info.

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.
@Keithcat1
Copy link

Can this be murged? It would be useful to use this in other languages because it not only provides cross-platform speech, but handles WinRT on Windows in a convenient manner, which can't be done in less mature languages like Dart or Go.

@ndarilek
Copy link
Owner

ndarilek commented Sep 23, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants