prism-sys 0.1.3

Raw FFI bindings to the prism speech library
Documentation

Prismer

Rust bindings to prism, the platform-agnostic reader interface for speech and messages. Prism unifies screen readers and TTS engines (SAPI, NVDA, JAWS, VoiceOver, AVSpeech, speech-dispatcher, Orca, Android TTS, WebSpeech, and more) behind one API, so this crate gives your Rust application speech, braille, and screen reader output on every major platform.

Crates

  • prism-sys: raw FFI bindings to the prism C API, no_std.
  • prismer: safe idiomatic wrapper.

Quick start

let prism = prismer::Prism::new()?;
let backend = prism.create_best()?;
backend.speak("Hello from Rust!", false)?;

See crates/prismer/examples/speak.rs for a fuller example, including feature detection and waiting for speech to finish.

Picking a backend

There are four ways to get a backend:

Method Backend state Initialized on return
create_best() private to you yes
create(id) private to you no, call initialize()
acquire_best() shared with other callers yes
acquire(id) shared with other callers maybe, call initialize() and treat AlreadyInitialized as success

Use create_best() unless you have a specific reason not to. The acquire family returns a cached instance, so a voice, rate, or pitch set through one handle is visible through every other handle to that backend. Reach for it only when sharing that state is what you want.

Building

Prism is vendored as a git submodule and built automatically by prism-sys's build script, so all you need is a C++23 toolchain and CMake 3.24+:

git clone --recurse-submodules https://github.com/trypsynth/prismer
cd prismer
cargo build

If you cloned without --recurse-submodules, run git submodule update --init first.

To link against a prebuilt prism instead (skipping the CMake build), set PRISM_LIB_DIR to the directory containing the library:

set PRISM_LIB_DIR=path\to\prism\build
cargo build

By default prism is built and linked as a shared library; note that your application must be able to find it (next to the executable on Windows, or on the loader path elsewhere) at runtime. Enable the static feature to build and link it statically instead. The build script links the C++ runtime and the system libraries prism needs for you.

On Windows, a static build has one more step. The screen reader backends import DLLs that most machines don't have, so your program has to delay load them, or it won't start without all of them installed. Linker flags only apply to the crate that sets them, so this has to happen in your application's own build.rs. prismer passes the list on to you:

fn main() {
	if let Ok(dlls) = std::env::var("DEP_PRISMER_DELAY_LOAD_DLLS") {
		for dll in dlls.split(';').filter(|dll| !dll.is_empty()) {
			println!("cargo:rustc-link-arg=/DELAYLOAD:{dll}");
		}
	}
}

The linker may warn that a couple of these were ignored because nothing imports from them. That's expected: those are bridges for backends that only exist on other platforms.

Custom backends

You can write a backend in Rust and register it alongside prism's own. Implement CustomBackend, declare the operations you implemented, and freeze a registry:

use prismer::{CustomBackend, Features, Prism, RegistryBuilder};

struct Printer;

impl CustomBackend for Printer {
	fn speak(&mut self, text: &str, interrupt: bool) -> prismer::Result<()> {
		println!("{text}");
		Ok(())
	}
}

let mut builder = RegistryBuilder::new()?;
let id = builder.add_backend("Example Printer", 10, Features::SPEAK, || Some(Printer))?;
let registry = builder.freeze()?;
let prism = Prism::builder().registry(&registry).build()?;
let backend = prism.create(id)?;

Every trait method defaults to NotImplemented, and the vtable handed to prism gets a pointer only for the features you declare, so the two can never disagree. The factory closure runs once per instance, so instances never share state. See crates/prismer/examples/custom_backend.rs for a full program.

RegistryBuilder::add_library loads a prism plugin shared library and registers the backends it supplies.

Logging

The log module wraps prism's process-wide logger:

prismer::log::set_level(prismer::log::Level::Warn);
prismer::log::set_handler(|level, source, message| {
	eprintln!("[{level:?}] {source}: {message}");
});

The handler runs on prism's logging thread, so keep it quick. It is leaked on purpose: prism may still deliver to a replaced handler, so there is no safe moment to free one.

Status

Early draft, but the C API is fully covered: context init and configuration (including availability callbacks), registry queries, backend creation and acquisition, speech, braille, output, playback control, volume, rate, pitch, voice enumeration and selection, audio format queries, speak-to-memory with a closure callback, custom backends and plugin libraries through the registry builder, and the logging API.

License

MIT. Prism itself is MPL-2.0.