endurox-rs 0.1.334

Dev middleware utilities for Enduro/X projects (Rust bindings).
Rust bindings for Enduro/X.

# Runtime modes

The default build has no async-runtime dependency. Use `AtmiCtx::tpcall` for
the normal blocking XATMI call path, or `AtmiCtx::tpacall` plus
`AtmiCtx::tpgetrply` when managing Enduro/X call descriptors directly.

Async support uses a runtime-neutral call state machine with an explicitly
selected reply-fd driver. Async features also enable `ctx-send`, ensuring each
adapter owns a distinct Enduro/X Object API context. Tokio-native integration
is optional:

```toml
[dependencies]
endurox-rs = { version = "0.1", features = ["tokio"] }
tokio = { version = "1", features = ["macros", "net", "rt", "time"] }
```

For an executor-independent driver backed by the `async-io` reactor instead:

```toml
endurox-rs = { version = "0.1", features = ["async-io"] }
```

The `async-io` adapter's futures can be polled by Tokio, smol, async-std, or
another standard Rust executor. Enabling both `tokio` and `async-io` is also
supported; the adapter type determines which reactor a context uses.

Async waiting requires an Enduro/X build whose `ndrx_config.h` selects
`EX_USE_EPOLL` or `EX_USE_KQUEUE`. On other queue backends adapter construction
returns `TPEINVAL`; use the blocking API from the runtime's blocking-task
facility and create the complete `AtmiCtx` inside that task.

`AtmiCtx` is deliberately `!Sync`, so its async call futures are `!Send`. Await
them directly on a current-thread runtime or a `tokio::task::LocalSet`, rather
than passing them to `tokio::spawn`:

```rust,no_run
use endurox_rs::AtmiCtx;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let ctx = AtmiCtx::new()?;
    ctx.tpinit()?;
    let ctx = ctx.into_tokio()?;

    let request = ctx.tpalloc_carray(b"request")?;
    let mut response = ctx.tpalloc_carray(&[])?;
    ctx.tpcall("MY_SERVICE", &request, &mut response, 0).await?;

    drop(response);
    drop(request);
    ctx.tpterm()?;
    Ok(())
}
```

The async signatures match the blocking ones. There is no per-call timeout
argument, because XATMI has none: timeouts come from `NDRX_TOUT`, `tptoutset`
and `tpsblktime`, and the async path reads the effective value with
`tpgblktime` before each `tpacall`. For a one-off override, use the same idiom
you would use before a blocking call:

```rust,no_run
# use endurox_rs::{AtmiCtx, TPBLK_NEXT};
# async fn f(ctx: &endurox_rs::TokioAtmiCtx, req: &endurox_rs::TypedBuffer<'_>,
#            rsp: &mut endurox_rs::TypedBuffer<'_>) -> Result<(), endurox_rs::AtmiError> {
ctx.tpsblktime(3, TPBLK_NEXT)?;
ctx.tpcall("MY_SERVICE", req, rsp, 0).await?;
# Ok(())
# }
```

Concurrent async calls on one context share a single reactor registration.
Replies are demultiplexed by call descriptor using `tpgetrply(TPGETANY)`, so a
reply wakes exactly the future waiting for it, and no reply is ever diverted
into Enduro/X's in-memory queue where the reply fd could not signal it again.

Dropping `AsyncAtmiCtx::tpcall` cancels its Enduro/X call descriptor; dropping
`AsyncAtmiCtx::tpgetrply` leaves its caller-owned descriptor pending so it can
be awaited again or cancelled explicitly. Multiple adapters may share one
executor thread; each owns a separate Enduro/X context and reply queue.

The generic form is `AsyncAtmiCtx<D>`, where `D: AsyncReplyDriver`. The provided
drivers are `TokioReplyDriver` and `AsyncIoReplyDriver`. Because the adapter
owns `AtmiCtx`, a context cannot accidentally be registered with two reactors.

# Server dispatch threads

`AtmiCtx::tp_run` enables Enduro/X's multithread-capable integration mode. When
`maxdispatchthreads` is greater than one, `mindispatchthreads` worker threads
are created and service callbacks may execute concurrently on any of them.
Each callback receives a worker-local `AtmiCtx`; with `ctx-send` it temporarily
uses that worker's OAPI context and restores the worker TLS before returning.
With `maxdispatchthreads=1`, callbacks stay on the `tp_run` main thread and use
the owning server context; worker init/done hooks are intentionally not called.
With `mindispatchthreads=1` and `maxdispatchthreads>1`, one worker is created and
its thread init/done hooks are called normally.

Rust integration also uses `ATMI_SRVLIB_NOLONGJUMP`, so `tpreturn` and
`tpforward` return through Rust normally instead of performing a C `longjmp`
across Rust stack frames. Service handlers must protect any shared application
state because the same handler function can run concurrently.

# Testing

Sync mode tests:

```
$ cargo test
```

Context-migration tests:

```
$ cargo test --features ctx-send
```

Tokio API tests:

```
$ cargo test --features tokio
```

Executor-independent async API tests:

```
$ cargo test --features async-io
```