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