# fswtch

[![Crates.io Version](https://img.shields.io/crates/v/fswtch)](https://crates.io/crates/fswtch)
[![fswtch-sys](https://img.shields.io/crates/v/fswtch-sys?label=fswtch-sys)](https://crates.io/crates/fswtch-sys)

Rust bindings and helper APIs for writing FreeSWITCH modules.

This workspace is intentionally split into three crates:

- `fswtch`: safe-ish helpers for module exports, module interface creation, API command registration, stream writes, status conversion, and example logging.
- `fswtch-sys`: raw FreeSWITCH ABI bindings generated with bindgen.
- `fswtch-src`: packaged FreeSWITCH headers used by default bundled builds.

## Wrapper API

The `fswtch` crate provides a small higher-level layer over the raw FreeSWITCH ABI. It focuses on the parts that every module needs first:

- `module_exports!` declares the exported FreeSWITCH module table.
- `module_load!` generates the FreeSWITCH load callback while giving the body a `ModuleBuilder`.
- `Module::create` builds the loader-owned module interface from raw load callback arguments for lower-level integrations.
- `Module::add_api`, `Module::add_application`, `Module::add_chat_application`, and `Module::add_endpoint` register common FreeSWITCH interfaces without hand-writing interface allocation and field assignment.
- `ModuleBuilder` chains module and interface registration in load callbacks.
- `api_callback!`, `app_callback!`, and `chat_callback!` generate FreeSWITCH ABI callbacks while giving the callback body typed wrapper values.
- `Session` wraps common application callback operations such as answering, sleeping, and file playback.
- `Stream` wraps `switch_stream_handle_t` for byte and string responses.
- `command_text` converts nullable FreeSWITCH callback command pointers into trimmed Rust strings.
- `Event` and `EventRef` wrap custom event creation, headers, firing, cleanup, and inbound event header reads while accepting Rust strings.
- Module registration, media bug config, session playback, XML helpers, and event helpers convert Rust strings to C strings inside `fswtch`.
- `Status`, `SwitchError`, and `status_to_result` convert common FreeSWITCH status handling into Rust `Result` values.
- `LogLevel`, `log`, and convenience helpers such as `log_info`, `log_warning`, `log_error`, and `log_debug1` through `log_debug10` route module logs through FreeSWITCH logging.
- `MediaBugConfig`, `MediaBugFlags`, `MediaBugHandler`, and `attach_media_bug` provide a higher-level media bug API for bidirectional read/write audio stream callbacks and read/write replacement hooks.

The wrapper does not try to hide the full ABI yet. Examples use `fswtch::sys` directly where FreeSWITCH exposes interfaces that still need raw pointer setup, such as endpoint I/O routine tables and lifecycle callbacks. Keep those raw calls narrow, document the callback and ownership assumptions, and prefer adding focused helpers to `fswtch` when the same unsafe pattern appears in more than one module.

Media bug handlers are owned by FreeSWITCH until the close callback. A module can implement `MediaBugHandler` to observe read and write frames, mutate replacement frames, or pull frames explicitly through `MediaBugContext`:

```rust
struct Meter;

impl fswtch::MediaBugHandler for Meter {
    fn on_read(
        &mut self,
        _ctx: &mut fswtch::MediaBugContext<'_>,
        frame: fswtch::MediaFrame<'_>,
    ) -> fswtch::MediaBugAction {
        fswtch::log_debug("mod_meter", format!("read {} bytes", frame.data_len()));
        fswtch::MediaBugAction::Continue
    }
}

let config = fswtch::MediaBugConfig::new(
    "mod_meter",
    "read-write",
    fswtch::MediaBugFlags::READ_STREAM
        | fswtch::MediaBugFlags::WRITE_STREAM
        | fswtch::MediaBugFlags::NO_PAUSE,
)?;

fswtch::attach_media_bug(session, config, Meter)?;
```

## Build

Default builds use the bundled FreeSWITCH headers from `fswtch-src`:

```sh
cargo check -p fswtch-sys
cargo check -p fswtch --examples
cargo test --workspace
cargo fmt --all --check
cargo clippy --workspace --all-targets
```

The default `bundled` feature only generates Rust bindings from packaged headers. It does not compile or statically link FreeSWITCH.

To generate bindings from a configured local FreeSWITCH install:

```sh
FREESWITCH_INCLUDE_DIR=/usr/include/freeswitch \
  cargo check -p fswtch-sys --no-default-features --features bindgen
```

If link metadata is not available through `pkg-config`, set the library directory explicitly:

```sh
FREESWITCH_LIB_DIR=/usr/lib/freeswitch cargo build
```

Set `FREESWITCH_NO_PKG_CONFIG=1` to disable `pkg-config` probing.

## Docker Smoke Test

The repository includes a full smoke image that builds FreeSWITCH, builds every Rust example as a `cdylib`, installs the modules, starts FreeSWITCH, and verifies APIs through `fs_cli`.

```sh
docker build -t fswtch-freeswitch-smoke .
docker run --rm fswtch-freeswitch-smoke
```

Podman works too:

```sh
podman build -t fswtch-freeswitch-smoke .
podman run --rm fswtch-freeswitch-smoke
```

Successful output ends with:

```text
all fswtch example module checks passed
```

The smoke script enables `FSWTCH_AI_ALLOW_MOCK=1` so the local AI example can run without model files or OpenAI credentials.

## Module Shape

A minimal module exports a FreeSWITCH load callback:

```rust
fswtch::module_exports! {
    module = mod_hello,
    load = switch_module_load,
}
```

Use `module_load!` to create the typed load callback and register one or more APIs:

```rust
fswtch::module_load! {
    fn switch_module_load(module) for "mod_hello" {
        fswtch::log_info("mod_hello", "loading module");
        module.api(
            "rust_hello",
            "prints a Rust greeting",
            "rust_hello",
            hello_api,
        )
    }
}
```

Examples use `fswtch::log_info` and `fswtch::log_error`, which route through FreeSWITCH logging.

## Examples

All examples live in `crates/fswtch/examples` and are compiled as FreeSWITCH modules.

Basic module and API patterns:

- `mod_hello`: minimal API command.
- `mod_api_suite`: multiple API commands in one module.
- `mod_stream_tools`: stream responses and command argument parsing.
- `mod_lifecycle`: load, runtime, and shutdown callbacks.

Operational and integration patterns:

- `mod_async_job_queue`: background worker queue with bounded result history.
- `mod_event_sink`: JSON-to-custom-event bridge.
- `mod_http_webhook`: queued plain HTTP webhook delivery.
- `mod_registration_check`: async registration validation and custom event emission.
- `mod_rate_limiter`: token-bucket style API rate limiting with bounded cardinality.
- `mod_metrics`: Prometheus-style metrics output with bounded cardinality.
- `mod_config_xml`: FreeSWITCH XML config loading and reload.
- `mod_cdr_enricher`: CDR JSON enrichment and custom event emission.

FreeSWITCH interface skeletons:

- `mod_app_playback_control`: dialplan application interface that answers and plays a supplied target.
- `mod_media_bug_meter`: media bug application that counts observed read/write-stream audio frames.
- `mod_endpoint_skeleton`: endpoint interface registration skeleton.
- `mod_chatbot_bridge`: chat application interface that emits chatbot bridge events.

AI and media integration:

- `mod_remote_vad`: async websocket VAD worker with custom event reporting.
- `mod_local_ai_bridge`: local ASR/TTS integration boundary plus OpenAI Responses API NLP calls.

## Local AI Example

`mod_local_ai_bridge` exposes:

- `rust_local_ai_status`
- `rust_local_asr <pcm16le-file>`
- `rust_local_tts <text>`
- `rust_local_nlp <prompt>`
- `rust_local_nlp_sync <prompt>`

Environment variables:

- `FSWTCH_ASR_ONNX`: local ASR ONNX model path.
- `FSWTCH_TTS_ONNX`: local TTS ONNX model path.
- `OPENAI_API_KEY`: enables OpenAI NLP calls.
- `OPENAI_MODEL`: defaults to `gpt-5.1`.
- `OPENAI_BASE_URL`: defaults to `https://api.openai.com/v1`.
- `FSWTCH_AI_ALLOW_MOCK=1`: allows smoke-test fallback behavior when models or API credentials are absent.

For production, do not set `FSWTCH_AI_ALLOW_MOCK`; provide real model paths and API credentials. The example isolates the ORT boundary, but real ASR/TTS inference still needs the tensor contracts for the chosen ONNX models.

## Production Notes

The examples are production-oriented examples, not drop-in production services. Before deploying a module, review:

- Session lifetime and locking for any work that touches a live `switch_core_session_t` outside the original callback.
- Backpressure and queue limits for background work.
- Timeout and retry policy for network integrations.
- Secret handling for API keys and webhook credentials.
- Cardinality limits for metrics, rate limiters, and per-call state.
- Cleanup and ownership rules for FreeSWITCH events, media bugs, XML roots, and allocated user data.
- Real model initialization and tensor validation for ORT-backed ASR/TTS.

Unsafe blocks are kept small and local to FFI operations. Public unsafe APIs in the wrapper should document a `# Safety` contract.

## Repository Layout

- `crates/fswtch`: wrapper API and compile-checked Rust module examples.
- `crates/fswtch-sys`: raw generated FreeSWITCH bindings and bindgen build script.
- `crates/fswtch-src`: packaged FreeSWITCH headers.
- `docker/fswtch`: smoke-test FreeSWITCH config and verification script.
- `Dockerfile`: full FreeSWITCH smoke-test image.
- `freeswitch/`: vendored upstream FreeSWITCH source context.

The vendored FreeSWITCH trees are third-party inputs. Avoid reformatting or refactoring them unless intentionally updating vendored FreeSWITCH content.

## Publishing

Publish crates in dependency order:

```sh
cargo publish -p fswtch-src
cargo publish -p fswtch-sys
cargo publish -p fswtch
```

Before publishing, run the focused Rust checks above and the Docker smoke test when example behavior changed.
