Skip to main content

actr_framework/guest/
mod.rs

1//! Guest-side runtime module.
2//!
3//! Provides the unified [`entry!`] macro and platform-specific runtime
4//! glue. Actor developers write one `entry!(MyActor)`; the macro selects
5//! the correct ABI at compile time based on the target.
6//!
7//! # Execution contract
8//!
9//! - One loaded guest instance corresponds to one logical actor instance.
10//! - The retired 0.1.0 WASM world is rejected at load. DynClib remains serial.
11//!   The sole supported 0.2.0 async WASM world may cooperatively interleave
12//!   distinct-conflict-key dispatches at host-import await points; same-key
13//!   and undeclared dispatches remain serialized by the host scheduler.
14//!
15//! # Supported platforms
16//!
17//! - **WASM Component Model** (`target_arch = "wasm32"`, no `web` feature):
18//!   wit-bindgen generates the `Guest` trait + `host` imports from
19//!   `core/framework/wit-v2/actr-workload.wit`; the [`entry!`] macro
20//!   produces an adapter that bridges the user's [`Workload`] impl into
21//!   the generated `Guest`. Targets `wasm32-wasip2` and requires
22//!   `wasm-component-ld 0.5.22+` as the linker (see
23//!   `experiments/component-spike-async/REPORT.md`).
24//! - **Web ABI / wasm-bindgen** (`target_arch = "wasm32"` + `feature = "web"`):
25//!   expands to a [`wasm_bindgen(start)`][wbgstart] bootstrap that wraps the
26//!   user [`Workload`] in `web::WebWorkloadAdapter` and hands it to
27//!   `actr_web_abi::host::register_workload`. Per Option U γ-unified §4.5
28//!   the same user source compiles against both wasm32 ABIs; only the
29//!   macro expansion differs. Targets `wasm32-unknown-unknown`.
30//! - **cdylib** (`feature = "cdylib"`): HostVTable function-pointer
31//!   bridge used for native shared-library guests (iOS / Android).
32//!
33//! [wbgstart]: https://rustwasm.github.io/wasm-bindgen/reference/attributes/on-rust-exports/start.html
34
35pub mod dynclib_abi;
36pub mod vtable;
37
38/// Decode a dynclib `WebRtcPeerStatus` discriminant (see
39/// [`dynclib_abi::webrtc_peer_status`]) into the framework enum. Used by the
40/// `entry!` macro when materialising [`crate::PeerEvent`] from `PeerEventV1`.
41pub fn peer_status_from_v1(status: u32) -> Option<crate::WebRtcPeerStatus> {
42    use crate::guest::dynclib_abi::webrtc_peer_status as st;
43    match status {
44        st::IDLE => Some(crate::WebRtcPeerStatus::Idle),
45        st::CONNECTING => Some(crate::WebRtcPeerStatus::Connecting),
46        st::CONNECTED => Some(crate::WebRtcPeerStatus::Connected),
47        st::RECOVERING => Some(crate::WebRtcPeerStatus::Recovering),
48        other => {
49            tracing::warn!(
50                discriminant = other,
51                "unknown dynclib WebRtcPeerStatus discriminant; dropping peer status"
52            );
53            None
54        }
55    }
56}
57
58// The Component Model wasm runtime glue is gated on `not(feature = "web")`
59// so the `wasm32-unknown-unknown` + `web` target (which routes through
60// `actr-web-abi` instead) does not link the wit-bindgen host imports that
61// only resolve in a `wasm32-wasip2` Component environment.
62#[cfg(all(target_arch = "wasm32", not(feature = "web")))]
63pub mod wasm;
64
65#[cfg(feature = "cdylib")]
66pub mod dynclib;
67
68// Re-exports used by the `entry!` macro so macro expansions under a
69// user crate can reference adapter / binding items via a stable path
70// without having to know the internal module layout.
71#[cfg(all(target_arch = "wasm32", not(feature = "web")))]
72#[doc(hidden)]
73pub mod __wasm_macro_support {
74    // The `entry!` macro expands inside the user's crate, so every name
75    // it references must be reachable from an external crate path. Both
76    // the adapter helpers and the generated WIT types are re-exported
77    // here so the macro has a single stable `$crate::guest::__wasm_macro_support::*`
78    // prefix to poke at.
79    pub use super::wasm::adapter::{
80        WorkloadCell, run_dispatch, run_on_credential_expiring, run_on_credential_renewed,
81        run_on_data_chunk, run_on_error, run_on_mailbox_backpressure, run_on_ready,
82        run_on_signaling_connected, run_on_signaling_connecting, run_on_signaling_disconnected,
83        run_on_start, run_on_stop, run_on_webrtc_connected, run_on_webrtc_connecting,
84        run_on_webrtc_disconnected, run_on_websocket_connected, run_on_websocket_connecting,
85        run_on_websocket_disconnected,
86    };
87    pub use super::wasm::generated::actr::workload::types::{
88        ActrError as WitActrError, ActrId as WitActrId, BackpressureEvent as WitBackpressureEvent,
89        CredentialEvent as WitCredentialEvent, DataChunk as WitDataChunk,
90        ErrorEvent as WitErrorEvent, InvocationCtx as WitInvocationCtx, PeerEvent as WitPeerEvent,
91        RpcEnvelope as WitRpcEnvelope,
92    };
93    pub use super::wasm::generated::exports::actr::workload::workload::Guest;
94}
95
96/// Generate Component Model exports for a [`Workload`][crate::Workload]
97/// type.
98///
99/// Platform ABI is auto-selected by target:
100///
101/// - `#[cfg(all(target_arch = "wasm32", not(feature = "web")))]` — expands
102///   to an `impl Guest for __ActrEntryAdapter { ... }` bridging the user's
103///   [`Workload`][crate::Workload] into the `actr:workload/workload`
104///   export contract. The runtime calls `Dispatcher::dispatch` through
105///   the `dispatch` export and every observation hook through its
106///   matching WIT export.
107/// - `#[cfg(all(target_arch = "wasm32", feature = "web"))]` — expands to a
108///   `#[wasm_bindgen(start)]` bootstrap that wraps the user workload in a
109///   `WebWorkloadAdapter` and calls `actr_web_abi::host::register_workload`.
110///   Only the 17 `#[wasm_bindgen]` entry points generated inside
111///   `actr-web-abi::host` are exported to the Service Worker host.
112/// - `#[cfg(feature = "cdylib")]` — expands to the legacy
113///   `actr_init` / `actr_handle` / `actr_shutdown` / `actr_free_response` C-ABI exports
114///   used by native shared-library hosts.
115///
116/// # Arguments
117///
118/// - `$workload_type`: type implementing
119///   `actr_framework::Workload + Send + Sync + 'static`.
120/// - `$init_expr` (optional): expression returning a fresh instance of
121///   `$workload_type`. Defaults to `<$workload_type as Default>::default()`.
122///
123/// # Usage
124///
125/// ```rust,ignore
126/// use actr_framework::entry;
127///
128/// entry!(EchoServiceWorkload<MyService>);
129///
130/// // Or with a custom constructor:
131/// entry!(
132///     EchoServiceWorkload<MyService>,
133///     EchoServiceWorkload::new(MyService::new())
134/// );
135/// ```
136#[macro_export]
137macro_rules! entry {
138    // Single-argument form: default-construct the workload.
139    ($workload_type:ty) => {
140        $crate::entry!($workload_type, <$workload_type as ::core::default::Default>::default());
141    };
142
143    // Two-argument form: caller supplies the init expression.
144    ($workload_type:ty, $init_expr:expr) => {
145        // ── WASM Component Model exports ──────────────────────────────────
146        //
147        // wit-bindgen generates an `exports::actr::workload::workload::Guest`
148        // trait with 17 async methods (one `dispatch` + sixteen hooks). We
149        // emit a single zero-sized adapter struct in user-crate scope and
150        // route every method through helpers in `actr_framework::guest::wasm::adapter`.
151        //
152        // Skipped when the `web` feature is on — that path uses the
153        // wasm-bindgen + `actr-web-abi` pipeline below instead of the
154        // Component Model exports.
155        #[cfg(all(target_arch = "wasm32", not(feature = "web")))]
156        const _: () = {
157            // Module-local singleton cell. Lazy-init on first call; subsequent
158            // dispatches reuse the same instance.
159            static __ACTR_WORKLOAD: $crate::guest::__wasm_macro_support::WorkloadCell<$workload_type> =
160                $crate::guest::__wasm_macro_support::WorkloadCell::new();
161
162            fn __actr_workload() -> &'static $workload_type {
163                __ACTR_WORKLOAD.get_or_init(|| -> $workload_type { $init_expr })
164            }
165
166            struct __ActrEntryAdapter;
167
168            impl $crate::guest::__wasm_macro_support::Guest for __ActrEntryAdapter {
169                async fn dispatch(
170                    envelope: $crate::guest::__wasm_macro_support::WitRpcEnvelope,
171                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
172                ) -> ::core::result::Result<
173                    ::std::vec::Vec<u8>,
174                    $crate::guest::__wasm_macro_support::WitActrError,
175                > {
176                    $crate::guest::__wasm_macro_support::run_dispatch(
177                        __actr_workload(),
178                        envelope,
179                        ctx,
180                    ).await
181                }
182
183                async fn on_start(
184                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
185                ) -> ::core::result::Result<
186                    (),
187                    $crate::guest::__wasm_macro_support::WitActrError,
188                > {
189                    $crate::guest::__wasm_macro_support::run_on_start(__actr_workload(), ctx).await
190                }
191
192                async fn on_ready(
193                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
194                ) -> ::core::result::Result<
195                    (),
196                    $crate::guest::__wasm_macro_support::WitActrError,
197                > {
198                    $crate::guest::__wasm_macro_support::run_on_ready(__actr_workload(), ctx).await
199                }
200
201                async fn on_stop(
202                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
203                ) -> ::core::result::Result<
204                    (),
205                    $crate::guest::__wasm_macro_support::WitActrError,
206                > {
207                    $crate::guest::__wasm_macro_support::run_on_stop(__actr_workload(), ctx).await
208                }
209
210                async fn on_error(
211                    event: $crate::guest::__wasm_macro_support::WitErrorEvent,
212                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
213                ) -> ::core::result::Result<
214                    (),
215                    $crate::guest::__wasm_macro_support::WitActrError,
216                > {
217                    $crate::guest::__wasm_macro_support::run_on_error(__actr_workload(), event, ctx).await
218                }
219
220                async fn on_signaling_connecting(
221                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
222                ) {
223                    $crate::guest::__wasm_macro_support::run_on_signaling_connecting(
224                        __actr_workload(),
225                        ctx,
226                    ).await
227                }
228
229                async fn on_signaling_connected(
230                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
231                ) {
232                    $crate::guest::__wasm_macro_support::run_on_signaling_connected(
233                        __actr_workload(),
234                        ctx,
235                    ).await
236                }
237
238                async fn on_signaling_disconnected(
239                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
240                ) {
241                    $crate::guest::__wasm_macro_support::run_on_signaling_disconnected(
242                        __actr_workload(),
243                        ctx,
244                    ).await
245                }
246
247                async fn on_websocket_connecting(
248                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
249                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
250                ) {
251                    $crate::guest::__wasm_macro_support::run_on_websocket_connecting(
252                        __actr_workload(),
253                        event,
254                        ctx,
255                    ).await
256                }
257
258                async fn on_websocket_connected(
259                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
260                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
261                ) {
262                    $crate::guest::__wasm_macro_support::run_on_websocket_connected(
263                        __actr_workload(),
264                        event,
265                        ctx,
266                    ).await
267                }
268
269                async fn on_websocket_disconnected(
270                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
271                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
272                ) {
273                    $crate::guest::__wasm_macro_support::run_on_websocket_disconnected(
274                        __actr_workload(),
275                        event,
276                        ctx,
277                    ).await
278                }
279
280                async fn on_webrtc_connecting(
281                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
282                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
283                ) {
284                    $crate::guest::__wasm_macro_support::run_on_webrtc_connecting(
285                        __actr_workload(),
286                        event,
287                        ctx,
288                    ).await
289                }
290
291                async fn on_webrtc_connected(
292                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
293                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
294                ) {
295                    $crate::guest::__wasm_macro_support::run_on_webrtc_connected(
296                        __actr_workload(),
297                        event,
298                        ctx,
299                    ).await
300                }
301
302                async fn on_webrtc_disconnected(
303                    event: $crate::guest::__wasm_macro_support::WitPeerEvent,
304                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
305                ) {
306                    $crate::guest::__wasm_macro_support::run_on_webrtc_disconnected(
307                        __actr_workload(),
308                        event,
309                        ctx,
310                    ).await
311                }
312
313                async fn on_credential_renewed(
314                    event: $crate::guest::__wasm_macro_support::WitCredentialEvent,
315                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
316                ) {
317                    $crate::guest::__wasm_macro_support::run_on_credential_renewed(
318                        __actr_workload(),
319                        event,
320                        ctx,
321                    ).await
322                }
323
324                async fn on_credential_expiring(
325                    event: $crate::guest::__wasm_macro_support::WitCredentialEvent,
326                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
327                ) {
328                    $crate::guest::__wasm_macro_support::run_on_credential_expiring(
329                        __actr_workload(),
330                        event,
331                        ctx,
332                    ).await
333                }
334
335                async fn on_mailbox_backpressure(
336                    event: $crate::guest::__wasm_macro_support::WitBackpressureEvent,
337                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
338                ) {
339                    $crate::guest::__wasm_macro_support::run_on_mailbox_backpressure(
340                        __actr_workload(),
341                        event,
342                        ctx,
343                    ).await
344                }
345
346                async fn on_data_chunk(
347                    chunk: $crate::guest::__wasm_macro_support::WitDataChunk,
348                    sender: $crate::guest::__wasm_macro_support::WitActrId,
349                    ctx: $crate::guest::__wasm_macro_support::WitInvocationCtx,
350                ) -> ::core::result::Result<
351                    (),
352                    $crate::guest::__wasm_macro_support::WitActrError,
353                > {
354                    $crate::guest::__wasm_macro_support::run_on_data_chunk(chunk, sender, ctx).await
355                }
356            }
357
358            $crate::guest::wasm::generated::export!(__ActrEntryAdapter with_types_in $crate::guest::wasm::generated);
359        };
360
361        // ── Web (wasm-bindgen + actr-web-abi) exports ─────────────────────
362        //
363        // Phase 6b: wrap the user workload in `WebWorkloadAdapter` and hand
364        // it to `actr_web_abi::host::register_workload` from a wasm-bindgen
365        // `start` hook. The 17 `#[wasm_bindgen]` entry points exported by
366        // `actr-web-abi::host` are the public surface the Service Worker
367        // dispatches into; they resolve through the registered adapter back
368        // into the user's `Workload` impl. See
369        // `bindings/web/docs/option-u-phase6-gamma-unified.zh.md` §4.5.
370        #[cfg(all(target_arch = "wasm32", feature = "web"))]
371        const _: () = {
372            // `wasm_bindgen(start)` functions are invoked once per module
373            // instantiation by the wasm-bindgen runtime. `register_workload`
374            // itself panics on double-registration, so wrapping it inside a
375            // `start` fn naturally enforces single-shot bootstrap.
376            //
377            // The attribute is referenced through its fully-qualified path
378            // so the user's crate does not need its own `wasm-bindgen`
379            // dependency — `actr-framework` re-exports the attribute through
380            // `web::__web_macro_support` under `feature = "web"`.
381            #[$crate::web::__web_macro_support::wasm_bindgen(start)]
382            fn __actr_web_bootstrap() {
383                let workload: $workload_type = $init_expr;
384                let adapter =
385                    $crate::web::__web_macro_support::WebWorkloadAdapter::new(workload);
386                $crate::web::__web_macro_support::register_workload(adapter);
387            }
388        };
389
390        // ── cdylib ABI exports ────────────────────────────────────────────
391        //
392        // The dynclib owns a Tokio runtime so guest futures and background
393        // tasks keep executing without borrowing the host's executor stack.
394        #[cfg(feature = "cdylib")]
395        const _: () = {
396            static mut __ACTR_WORKLOAD: Option<$workload_type> = None;
397            static mut __ACTR_VTABLE: Option<*const $crate::guest::vtable::HostVTable> = None;
398
399            /// Initialize actor
400            ///
401            /// Host calls this after dlopen, passing HostVTable and init payload.
402            /// Returns 0 on success, negative on error.
403            /// Repeated calls while an instance is active return `INIT_FAILED`.
404            /// A successful `actr_shutdown` resets the runtime for a subsequent
405            /// sequential initialization of the same loaded image.
406            #[unsafe(no_mangle)]
407            pub unsafe extern "C" fn actr_init(
408                vtable: *const $crate::guest::vtable::HostVTable,
409                init_ptr: *const u8,
410                init_len: usize,
411            ) -> i32 {
412                if vtable.is_null() {
413                    return $crate::guest::dynclib_abi::code::INIT_FAILED;
414                }
415
416                let init_bytes = if init_ptr.is_null() || init_len == 0 {
417                    &[][..]
418                } else {
419                    unsafe { std::slice::from_raw_parts(init_ptr, init_len) }
420                };
421
422                // TODO: `actr_init` currently only validates that `InitPayloadV1`
423                // is decodable. The payload fields themselves are not yet
424                // consumed by the guest runtime on the dynclib path. This is a
425                // legacy gap carried forward from the previous init model.
426                if $crate::guest::dynclib_abi::decode_message::<$crate::guest::dynclib_abi::InitPayloadV1>(
427                    init_bytes,
428                )
429                .is_err()
430                {
431                    return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR;
432                }
433
434                if $crate::guest::dynclib::initialize().is_err() {
435                    return $crate::guest::dynclib_abi::code::INIT_FAILED;
436                }
437
438                let workload: $workload_type = $init_expr;
439                unsafe {
440                    if __ACTR_WORKLOAD.is_some() {
441                        return $crate::guest::dynclib_abi::code::INIT_FAILED;
442                    }
443                    __ACTR_VTABLE = Some(vtable);
444                    __ACTR_WORKLOAD = Some(workload);
445                }
446                $crate::guest::dynclib_abi::code::SUCCESS
447            }
448
449            /// Handle one runtime ABI frame.
450            #[unsafe(no_mangle)]
451            pub unsafe extern "C" fn actr_handle(
452                req_ptr: *const u8,
453                req_len: usize,
454                resp_out: *mut *mut u8,
455                resp_len_out: *mut usize,
456            ) -> i32 {
457                use actr_protocol::prost::Message as ProstMessage;
458                use $crate::{MessageDispatcher, Workload};
459
460                // Get vtable
461                let vtable = match unsafe { __ACTR_VTABLE } {
462                    Some(vt) => vt,
463                    None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
464                };
465
466                // Read runtime frame
467                if req_ptr.is_null() {
468                    return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR;
469                }
470                let req_bytes = unsafe { std::slice::from_raw_parts(req_ptr, req_len) };
471
472                let frame = match $crate::guest::dynclib_abi::decode_message::<
473                    $crate::guest::dynclib_abi::AbiFrame,
474                >(req_bytes) {
475                    Ok(f) => f,
476                    Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
477                };
478
479                if frame.op == $crate::guest::dynclib_abi::op::GUEST_LIFECYCLE {
480                    let payload = match <$crate::guest::dynclib_abi::GuestLifecycleV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
481                        Ok(payload) => payload,
482                        Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
483                    };
484
485                    let bridge_token = payload.bridge_token;
486                    let ctx = match unsafe {
487                        $crate::guest::dynclib::context::DynclibContext::from_invocation(
488                            vtable,
489                            payload.ctx,
490                            bridge_token,
491                        )
492                    } {
493                        Ok(c) => c,
494                        Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
495                    };
496
497                    let workload = unsafe {
498                        match __ACTR_WORKLOAD.as_ref() {
499                            Some(w) => w,
500                            None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
501                        }
502                    };
503
504                    let lifecycle_result = match payload.hook {
505                        $crate::guest::dynclib_abi::lifecycle_hook::ON_START => {
506                            match $crate::guest::dynclib::block_on(
507                                bridge_token,
508                                workload.on_start(&ctx),
509                            ) {
510                                Ok(result) => result,
511                                Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
512                            }
513                        }
514                        $crate::guest::dynclib_abi::lifecycle_hook::ON_READY => {
515                            match $crate::guest::dynclib::block_on(
516                                bridge_token,
517                                workload.on_ready(&ctx),
518                            ) {
519                                Ok(result) => result,
520                                Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
521                            }
522                        }
523                        $crate::guest::dynclib_abi::lifecycle_hook::ON_STOP => {
524                            match $crate::guest::dynclib::block_on(
525                                bridge_token,
526                                workload.on_stop(&ctx),
527                            ) {
528                                Ok(result) => result,
529                                Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
530                            }
531                        }
532                        _ => return $crate::guest::dynclib_abi::code::UNSUPPORTED_OP,
533                    };
534
535                    let resp_bytes = match lifecycle_result {
536                        Ok(()) => match $crate::guest::dynclib_abi::success_reply(::std::vec::Vec::new()) {
537                            Ok(bytes) => bytes,
538                            Err(code) => return code,
539                        },
540                        Err(err) => match $crate::guest::dynclib_abi::error_reply(
541                            $crate::guest::dynclib_abi::code::HANDLE_FAILED,
542                            err.to_string().into_bytes(),
543                        ) {
544                            Ok(bytes) => bytes,
545                            Err(code) => return code,
546                        },
547                    };
548
549                    let resp_len = resp_bytes.len();
550                    let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
551                        Ok(l) => l,
552                        Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
553                    };
554                    let ptr = unsafe { std::alloc::alloc(layout) };
555                    if ptr.is_null() {
556                        return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
557                    }
558
559                    unsafe {
560                        std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
561                        *resp_out = ptr;
562                        *resp_len_out = resp_len;
563                    }
564
565                    return $crate::guest::dynclib_abi::code::SUCCESS;
566                }
567
568                if frame.op == $crate::guest::dynclib_abi::op::GUEST_HOOK {
569                    let payload = match <$crate::guest::dynclib_abi::GuestHookV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
570                        Ok(payload) => payload,
571                        Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
572                    };
573
574                    let bridge_token = payload.bridge_token;
575                    let ctx = match unsafe {
576                        $crate::guest::dynclib::context::DynclibContext::from_invocation(
577                            vtable,
578                            payload.ctx,
579                            bridge_token,
580                        )
581                    } {
582                        Ok(c) => c,
583                        Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
584                    };
585
586                    let workload = unsafe {
587                        match __ACTR_WORKLOAD.as_ref() {
588                            Some(w) => w,
589                            None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
590                        }
591                    };
592
593                    macro_rules! __actr_poll_unit {
594                        ($future:expr) => {{
595                            if $crate::guest::dynclib::block_on(bridge_token, $future).is_err() {
596                                return $crate::guest::dynclib_abi::code::HANDLE_FAILED;
597                            }
598                        }};
599                    }
600
601                    let peer_event = |peer: $crate::guest::dynclib_abi::PeerEventV1| {
602                        $crate::PeerEvent {
603                            peer: peer.peer,
604                            relayed: peer.relayed,
605                            status: peer
606                                .status
607                                .and_then($crate::guest::peer_status_from_v1),
608                        }
609                    };
610
611                    let timestamp = |ts: $crate::guest::dynclib_abi::TimestampV1| {
612                        std::time::UNIX_EPOCH
613                            + std::time::Duration::new(ts.seconds, ts.nanoseconds)
614                    };
615
616                    match payload.hook {
617                        $crate::guest::dynclib_abi::runtime_hook::ON_SIGNALING_CONNECTING => {
618                            __actr_poll_unit!(workload.on_signaling_connecting(Some(&ctx)));
619                        }
620                        $crate::guest::dynclib_abi::runtime_hook::ON_SIGNALING_CONNECTED => {
621                            __actr_poll_unit!(workload.on_signaling_connected(Some(&ctx)));
622                        }
623                        $crate::guest::dynclib_abi::runtime_hook::ON_SIGNALING_DISCONNECTED => {
624                            __actr_poll_unit!(workload.on_signaling_disconnected(&ctx));
625                        }
626                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBSOCKET_CONNECTING => {
627                            let event = match payload.peer {
628                                Some(peer) => peer_event(peer),
629                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
630                            };
631                            __actr_poll_unit!(workload.on_websocket_connecting(&ctx, &event));
632                        }
633                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBSOCKET_CONNECTED => {
634                            let event = match payload.peer {
635                                Some(peer) => peer_event(peer),
636                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
637                            };
638                            __actr_poll_unit!(workload.on_websocket_connected(&ctx, &event));
639                        }
640                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBSOCKET_DISCONNECTED => {
641                            let event = match payload.peer {
642                                Some(peer) => peer_event(peer),
643                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
644                            };
645                            __actr_poll_unit!(workload.on_websocket_disconnected(&ctx, &event));
646                        }
647                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBRTC_CONNECTING => {
648                            let event = match payload.peer {
649                                Some(peer) => peer_event(peer),
650                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
651                            };
652                            __actr_poll_unit!(workload.on_webrtc_connecting(&ctx, &event));
653                        }
654                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBRTC_CONNECTED => {
655                            let event = match payload.peer {
656                                Some(peer) => peer_event(peer),
657                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
658                            };
659                            __actr_poll_unit!(workload.on_webrtc_connected(&ctx, &event));
660                        }
661                        $crate::guest::dynclib_abi::runtime_hook::ON_WEBRTC_DISCONNECTED => {
662                            let event = match payload.peer {
663                                Some(peer) => peer_event(peer),
664                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
665                            };
666                            __actr_poll_unit!(workload.on_webrtc_disconnected(&ctx, &event));
667                        }
668                        $crate::guest::dynclib_abi::runtime_hook::ON_CREDENTIAL_RENEWED => {
669                            let event = match payload.credential {
670                                Some(credential) => $crate::CredentialEvent {
671                                    new_expiry: timestamp(credential.new_expiry),
672                                },
673                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
674                            };
675                            __actr_poll_unit!(workload.on_credential_renewed(&ctx, &event));
676                        }
677                        $crate::guest::dynclib_abi::runtime_hook::ON_CREDENTIAL_EXPIRING => {
678                            let event = match payload.credential {
679                                Some(credential) => $crate::CredentialEvent {
680                                    new_expiry: timestamp(credential.new_expiry),
681                                },
682                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
683                            };
684                            __actr_poll_unit!(workload.on_credential_expiring(&ctx, &event));
685                        }
686                        $crate::guest::dynclib_abi::runtime_hook::ON_MAILBOX_BACKPRESSURE => {
687                            let event = match payload.backpressure {
688                                Some(backpressure) => $crate::BackpressureEvent {
689                                    queue_len: backpressure.queue_len as usize,
690                                    threshold: backpressure.threshold as usize,
691                                },
692                                None => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
693                            };
694                            __actr_poll_unit!(workload.on_mailbox_backpressure(&ctx, &event));
695                        }
696                        _ => return $crate::guest::dynclib_abi::code::UNSUPPORTED_OP,
697                    }
698
699                    let resp_bytes = match $crate::guest::dynclib_abi::success_reply(::std::vec::Vec::new()) {
700                        Ok(bytes) => bytes,
701                        Err(code) => return code,
702                    };
703
704                    let resp_len = resp_bytes.len();
705                    let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
706                        Ok(l) => l,
707                        Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
708                    };
709                    let ptr = unsafe { std::alloc::alloc(layout) };
710                    if ptr.is_null() {
711                        return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
712                    }
713
714                    unsafe {
715                        std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
716                        *resp_out = ptr;
717                        *resp_len_out = resp_len;
718                    }
719
720                    return $crate::guest::dynclib_abi::code::SUCCESS;
721                }
722
723                if frame.op == $crate::guest::dynclib_abi::op::GUEST_DATA_CHUNK {
724                    let payload = match <$crate::guest::dynclib_abi::GuestDataChunkV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
725                        Ok(payload) => payload,
726                        Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
727                    };
728
729                    let resp_bytes = match $crate::guest::dynclib::context::dispatch_registered_stream(payload) {
730                        Ok(()) => match $crate::guest::dynclib_abi::success_reply(::std::vec::Vec::new()) {
731                            Ok(bytes) => bytes,
732                            Err(code) => return code,
733                        },
734                        Err(err) => match $crate::guest::dynclib_abi::error_reply(
735                            $crate::guest::dynclib_abi::code::HANDLE_FAILED,
736                            err.to_string().into_bytes(),
737                        ) {
738                            Ok(bytes) => bytes,
739                            Err(code) => return code,
740                        },
741                    };
742
743                    let resp_len = resp_bytes.len();
744                    let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
745                        Ok(l) => l,
746                        Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
747                    };
748                    let ptr = unsafe { std::alloc::alloc(layout) };
749                    if ptr.is_null() {
750                        return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
751                    }
752
753                    unsafe {
754                        std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
755                        *resp_out = ptr;
756                        *resp_len_out = resp_len;
757                    }
758
759                    return $crate::guest::dynclib_abi::code::SUCCESS;
760                }
761
762                if frame.op != $crate::guest::dynclib_abi::op::GUEST_HANDLE {
763                    return $crate::guest::dynclib_abi::code::UNSUPPORTED_OP;
764                }
765
766                let handle = match <$crate::guest::dynclib_abi::GuestHandleV1 as $crate::guest::dynclib_abi::AbiPayload>::decode_payload(&frame.payload) {
767                    Ok(handle) => handle,
768                    Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
769                };
770
771                let envelope = match actr_protocol::RpcEnvelope::decode(handle.rpc_envelope.as_slice()) {
772                    Ok(e) => e,
773                    Err(_) => return $crate::guest::dynclib_abi::code::PROTOCOL_ERROR,
774                };
775
776                let bridge_token = handle.bridge_token;
777                let ctx = match unsafe {
778                    $crate::guest::dynclib::context::DynclibContext::from_invocation(
779                        vtable,
780                        handle.ctx,
781                        bridge_token,
782                    )
783                } {
784                    Ok(c) => c,
785                    Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
786                };
787
788                // Get workload reference
789                let workload = unsafe {
790                    match __ACTR_WORKLOAD.as_ref() {
791                        Some(w) => w,
792                        None => return $crate::guest::dynclib_abi::code::INIT_FAILED,
793                    }
794                };
795
796                // Route and execute via MessageDispatcher
797                type Dispatcher = <$workload_type as Workload>::Dispatcher;
798
799                let resp_result = match $crate::guest::dynclib::block_on(
800                    bridge_token,
801                    Dispatcher::dispatch(workload, envelope, &ctx),
802                ) {
803                    Ok(result) => result,
804                    Err(_) => return $crate::guest::dynclib_abi::code::HANDLE_FAILED,
805                };
806
807                let resp_bytes = match resp_result {
808                    Ok(b) => match $crate::guest::dynclib_abi::success_reply(b.to_vec()) {
809                        Ok(bytes) => bytes,
810                        Err(code) => return code,
811                    },
812                    Err(err) => match $crate::guest::dynclib_abi::error_reply(
813                        $crate::guest::dynclib_abi::code::HANDLE_FAILED,
814                        err.to_string().into_bytes(),
815                    ) {
816                        Ok(bytes) => bytes,
817                        Err(code) => return code,
818                    },
819                };
820
821                // Allocate response buffer on guest heap
822                let resp_len = resp_bytes.len();
823                let layout = match std::alloc::Layout::from_size_align(resp_len.max(1), 1) {
824                    Ok(l) => l,
825                    Err(_) => return $crate::guest::dynclib_abi::code::GENERIC_ERROR,
826                };
827                let ptr = unsafe { std::alloc::alloc(layout) };
828                if ptr.is_null() {
829                    return $crate::guest::dynclib_abi::code::GENERIC_ERROR;
830                }
831
832                unsafe {
833                    std::ptr::copy_nonoverlapping(resp_bytes.as_ptr(), ptr, resp_len);
834                    *resp_out = ptr;
835                    *resp_len_out = resp_len;
836                }
837
838                $crate::guest::dynclib_abi::code::SUCCESS
839            }
840
841            /// Stop background work before the host unloads this library.
842            ///
843            /// A non-success result means teardown could not prove that guest
844            /// code stopped; the host must keep the library mapped.
845            #[unsafe(no_mangle)]
846            pub unsafe extern "C" fn actr_shutdown() -> i32 {
847                let shutdown_result = $crate::guest::dynclib::shutdown();
848                $crate::guest::dynclib::context::clear_stream_callbacks();
849                unsafe {
850                    __ACTR_WORKLOAD = None;
851                    __ACTR_VTABLE = None;
852                }
853
854                match shutdown_result {
855                    Ok(()) => $crate::guest::dynclib_abi::code::SUCCESS,
856                    Err(_) => $crate::guest::dynclib_abi::code::GENERIC_ERROR,
857                }
858            }
859
860            /// Free guest-allocated response buffer
861            ///
862            /// Host calls this after using the response data returned by `actr_handle`.
863            #[unsafe(no_mangle)]
864            pub unsafe extern "C" fn actr_free_response(ptr: *mut u8, len: usize) {
865                if ptr.is_null() || len == 0 {
866                    return;
867                }
868                let layout = match std::alloc::Layout::from_size_align(len, 1) {
869                    Ok(l) => l,
870                    Err(_) => return,
871                };
872                unsafe { std::alloc::dealloc(ptr, layout) };
873            }
874        };
875    };
876}
877
878#[cfg(test)]
879mod tests {
880    use super::*;
881
882    #[test]
883    fn peer_status_from_v1_drops_unknown_discriminants() {
884        use crate::guest::dynclib_abi::webrtc_peer_status as st;
885
886        assert_eq!(
887            peer_status_from_v1(st::IDLE),
888            Some(crate::WebRtcPeerStatus::Idle)
889        );
890        assert_eq!(
891            peer_status_from_v1(st::CONNECTING),
892            Some(crate::WebRtcPeerStatus::Connecting)
893        );
894        assert_eq!(
895            peer_status_from_v1(st::CONNECTED),
896            Some(crate::WebRtcPeerStatus::Connected)
897        );
898        assert_eq!(
899            peer_status_from_v1(st::RECOVERING),
900            Some(crate::WebRtcPeerStatus::Recovering)
901        );
902        assert_eq!(peer_status_from_v1(u32::MAX), None);
903    }
904}