Skip to main content

actr_framework/guest/
dynclib_abi.rs

1//! DynClib-only C ABI for actr workloads.
2//!
3//! This module is the handwritten C ABI used by the DynClib workload variant
4//! (loaded via dlopen). The WASM variant does NOT consume these types — it
5//! goes through wit-bindgen-generated code against `core/framework/wit/actr-workload.wit`.
6//!
7//! Do NOT add wasm-path code paths here. Do NOT reference this module from
8//! the wasm guest adapter. This module is kept in sync with the WIT contract
9//! by `tools/wit-lint`.
10
11use crate::Dest;
12use actr_protocol::prost::Message as ProstMessage;
13use actr_protocol::{ActrError, ActrId, ActrType, DataStream, PayloadType};
14
15/// ABI error codes.
16pub mod code {
17    /// Operation succeeded.
18    pub const SUCCESS: i32 = 0;
19    /// Generic unrecoverable error.
20    pub const GENERIC_ERROR: i32 = -1;
21    /// Initialization failed.
22    pub const INIT_FAILED: i32 = -2;
23    /// Message handling failed.
24    pub const HANDLE_FAILED: i32 = -3;
25    /// Memory allocation failed.
26    pub const ALLOC_FAILED: i32 = -4;
27    /// Protocol / codec error.
28    pub const PROTOCOL_ERROR: i32 = -5;
29    /// Guest-provided reply buffer is too small.
30    pub const BUFFER_TOO_SMALL: i32 = -6;
31    /// Unsupported runtime operation code.
32    pub const UNSUPPORTED_OP: i32 = -7;
33}
34
35/// Internal ABI version numbers.
36pub mod version {
37    /// ABI version 1.
38    pub const V1: u32 = 1;
39}
40
41/// Runtime operation codes carried inside [`AbiFrame`].
42pub mod op {
43    pub const HOST_CALL: u32 = 1;
44    pub const HOST_TELL: u32 = 2;
45    pub const HOST_CALL_RAW: u32 = 3;
46    pub const HOST_DISCOVER: u32 = 4;
47    pub const HOST_REGISTER_STREAM: u32 = 5;
48    pub const HOST_UNREGISTER_STREAM: u32 = 6;
49    pub const HOST_SEND_DATA_STREAM: u32 = 7;
50    pub const GUEST_HANDLE: u32 = 101;
51    pub const GUEST_DATA_STREAM: u32 = 102;
52    pub const GUEST_LIFECYCLE: u32 = 103;
53    pub const GUEST_HOOK: u32 = 104;
54}
55
56/// Lifecycle hook identifiers carried by [`GuestLifecycleV1`].
57pub mod lifecycle_hook {
58    pub const ON_START: u32 = 1;
59    pub const ON_READY: u32 = 2;
60    pub const ON_STOP: u32 = 3;
61}
62
63/// Observation hook identifiers carried by [`GuestHookV1`].
64pub mod runtime_hook {
65    pub const ON_SIGNALING_CONNECTING: u32 = 1;
66    pub const ON_SIGNALING_CONNECTED: u32 = 2;
67    pub const ON_SIGNALING_DISCONNECTED: u32 = 3;
68    pub const ON_WEBSOCKET_CONNECTING: u32 = 4;
69    pub const ON_WEBSOCKET_CONNECTED: u32 = 5;
70    pub const ON_WEBSOCKET_DISCONNECTED: u32 = 6;
71    pub const ON_WEBRTC_CONNECTING: u32 = 7;
72    pub const ON_WEBRTC_CONNECTED: u32 = 8;
73    pub const ON_WEBRTC_DISCONNECTED: u32 = 9;
74    pub const ON_CREDENTIAL_RENEWED: u32 = 10;
75    pub const ON_CREDENTIAL_EXPIRING: u32 = 11;
76    pub const ON_MAILBOX_BACKPRESSURE: u32 = 12;
77}
78
79/// Dedicated payload used by `actr_init`.
80#[derive(Clone, PartialEq, prost::Message)]
81pub struct InitPayloadV1 {
82    #[prost(uint32, tag = "1")]
83    pub version: u32,
84    #[prost(string, tag = "2")]
85    pub actr_type: String,
86    #[prost(bytes = "vec", tag = "3")]
87    pub credential: Vec<u8>,
88    #[prost(bytes = "vec", tag = "4")]
89    pub actor_id: Vec<u8>,
90    #[prost(uint32, tag = "5")]
91    pub realm_id: u32,
92}
93
94/// Runtime frame used by both host->guest and guest->host invocation.
95///
96/// TODO: This type is temporarily `pub` because `actr_hyper` still performs
97/// cross-crate host-side ABI encoding and decoding through
98/// `actr_framework::guest::dynclib_abi`. Once the shared runtime ABI types
99/// are moved to a better ownership boundary, narrow this visibility to the
100/// intended internal-only surface.
101#[derive(Clone, PartialEq, prost::Message)]
102pub struct AbiFrame {
103    #[prost(uint32, tag = "1")]
104    pub abi_version: u32,
105    #[prost(uint32, tag = "2")]
106    pub op: u32,
107    #[prost(bytes = "vec", tag = "3")]
108    pub payload: Vec<u8>,
109}
110
111/// Runtime reply frame.
112///
113/// TODO: Keep visibility aligned with [`AbiFrame`]. This is public only as a
114/// temporary crate-topology workaround while host-side runtime code lives in
115/// `actr_hyper`.
116#[derive(Clone, PartialEq, prost::Message)]
117pub struct AbiReply {
118    #[prost(uint32, tag = "1")]
119    pub abi_version: u32,
120    #[prost(int32, tag = "2")]
121    pub status: i32,
122    #[prost(bytes = "vec", tag = "3")]
123    pub payload: Vec<u8>,
124}
125
126/// Invocation context injected by Hyper before entering guest handle logic.
127#[derive(Clone, PartialEq, prost::Message)]
128pub struct InvocationContextV1 {
129    #[prost(message, required, tag = "1")]
130    pub self_id: ActrId,
131    #[prost(message, optional, tag = "2")]
132    pub caller_id: Option<ActrId>,
133    #[prost(string, tag = "3")]
134    pub request_id: String,
135}
136
137/// Runtime host->guest handle payload.
138#[derive(Clone, PartialEq, prost::Message)]
139pub struct GuestHandleV1 {
140    #[prost(message, required, tag = "1")]
141    pub ctx: InvocationContextV1,
142    #[prost(bytes = "vec", tag = "2")]
143    pub rpc_envelope: Vec<u8>,
144}
145
146/// Runtime host->guest DataStream payload.
147#[derive(Clone, PartialEq, prost::Message)]
148pub struct GuestDataStreamV1 {
149    #[prost(message, required, tag = "1")]
150    pub chunk: DataStream,
151    #[prost(message, required, tag = "2")]
152    pub sender: ActrId,
153}
154
155/// Runtime host->guest lifecycle hook payload.
156#[derive(Clone, PartialEq, prost::Message)]
157pub struct GuestLifecycleV1 {
158    #[prost(message, required, tag = "1")]
159    pub ctx: InvocationContextV1,
160    #[prost(uint32, tag = "2")]
161    pub hook: u32,
162}
163
164/// Wall-clock timestamp represented as seconds + nanoseconds since Unix epoch.
165#[derive(Clone, PartialEq, prost::Message)]
166pub struct TimestampV1 {
167    #[prost(uint64, tag = "1")]
168    pub seconds: u64,
169    #[prost(uint32, tag = "2")]
170    pub nanoseconds: u32,
171}
172
173/// Peer-scoped event payload for WebSocket / WebRTC hooks.
174#[derive(Clone, PartialEq, prost::Message)]
175pub struct PeerEventV1 {
176    #[prost(message, required, tag = "1")]
177    pub peer: ActrId,
178    #[prost(bool, optional, tag = "2")]
179    pub relayed: Option<bool>,
180}
181
182/// Credential lifecycle event payload.
183#[derive(Clone, PartialEq, prost::Message)]
184pub struct CredentialEventV1 {
185    #[prost(message, required, tag = "1")]
186    pub new_expiry: TimestampV1,
187}
188
189/// Mailbox backpressure event payload.
190#[derive(Clone, PartialEq, prost::Message)]
191pub struct BackpressureEventV1 {
192    #[prost(uint64, tag = "1")]
193    pub queue_len: u64,
194    #[prost(uint64, tag = "2")]
195    pub threshold: u64,
196}
197
198/// Runtime host->guest observation hook payload.
199#[derive(Clone, PartialEq, prost::Message)]
200pub struct GuestHookV1 {
201    #[prost(message, required, tag = "1")]
202    pub ctx: InvocationContextV1,
203    #[prost(uint32, tag = "2")]
204    pub hook: u32,
205    #[prost(message, optional, tag = "3")]
206    pub peer: Option<PeerEventV1>,
207    #[prost(message, optional, tag = "4")]
208    pub credential: Option<CredentialEventV1>,
209    #[prost(message, optional, tag = "5")]
210    pub backpressure: Option<BackpressureEventV1>,
211}
212
213/// ABI-level destination encoding (replaces hand-rolled 0x00/0x01/0x02 byte protocol).
214#[derive(Clone, PartialEq, prost::Message)]
215pub struct DestV1 {
216    #[prost(oneof = "DestKind", tags = "1, 2, 3")]
217    pub kind: Option<DestKind>,
218}
219
220/// Destination variants carried inside [`DestV1`].
221#[derive(Clone, PartialEq, prost::Oneof)]
222pub enum DestKind {
223    #[prost(bool, tag = "1")]
224    Shell(bool),
225    #[prost(bool, tag = "2")]
226    Local(bool),
227    #[prost(message, tag = "3")]
228    Actor(ActrId),
229}
230
231impl DestV1 {
232    /// Construct a shell destination.
233    pub fn shell() -> Self {
234        Self {
235            kind: Some(DestKind::Shell(true)),
236        }
237    }
238
239    /// Construct a local destination.
240    pub fn local() -> Self {
241        Self {
242            kind: Some(DestKind::Local(true)),
243        }
244    }
245
246    /// Construct an actor destination.
247    pub fn actor(id: ActrId) -> Self {
248        Self {
249            kind: Some(DestKind::Actor(id)),
250        }
251    }
252
253    /// Convert the ABI destination into the framework destination.
254    pub fn try_into_dest(self) -> Result<Dest, ActrError> {
255        match self.kind {
256            Some(DestKind::Shell(_)) => Ok(Dest::Shell),
257            Some(DestKind::Local(_)) => Ok(Dest::Local),
258            Some(DestKind::Actor(id)) => Ok(Dest::Actor(id)),
259            None => Err(ActrError::DecodeFailure(
260                "destination kind is missing".into(),
261            )),
262        }
263    }
264}
265
266/// Runtime guest->host call payload.
267#[derive(Clone, PartialEq, prost::Message)]
268pub struct HostCallV1 {
269    #[prost(string, tag = "1")]
270    pub route_key: String,
271    #[prost(message, required, tag = "2")]
272    pub dest: DestV1,
273    #[prost(bytes = "vec", tag = "3")]
274    pub payload: Vec<u8>,
275}
276
277/// Runtime guest->host tell payload.
278#[derive(Clone, PartialEq, prost::Message)]
279pub struct HostTellV1 {
280    #[prost(string, tag = "1")]
281    pub route_key: String,
282    #[prost(message, required, tag = "2")]
283    pub dest: DestV1,
284    #[prost(bytes = "vec", tag = "3")]
285    pub payload: Vec<u8>,
286}
287
288/// Runtime guest->host raw call payload.
289#[derive(Clone, PartialEq, prost::Message)]
290pub struct HostCallRawV1 {
291    #[prost(string, tag = "1")]
292    pub route_key: String,
293    #[prost(message, required, tag = "2")]
294    pub target: ActrId,
295    #[prost(bytes = "vec", tag = "3")]
296    pub payload: Vec<u8>,
297}
298
299/// Runtime guest->host discovery payload.
300#[derive(Clone, PartialEq, prost::Message)]
301pub struct HostDiscoverV1 {
302    #[prost(message, required, tag = "1")]
303    pub target_type: ActrType,
304}
305
306/// Runtime guest->host DataStream registration payload.
307#[derive(Clone, PartialEq, prost::Message)]
308pub struct HostRegisterStreamV1 {
309    #[prost(string, tag = "1")]
310    pub stream_id: String,
311}
312
313/// Runtime guest->host DataStream unregistration payload.
314#[derive(Clone, PartialEq, prost::Message)]
315pub struct HostUnregisterStreamV1 {
316    #[prost(string, tag = "1")]
317    pub stream_id: String,
318}
319
320/// Runtime guest->host DataStream send payload.
321#[derive(Clone, PartialEq, prost::Message)]
322pub struct HostSendDataStreamV1 {
323    #[prost(message, required, tag = "1")]
324    pub dest: DestV1,
325    #[prost(message, required, tag = "2")]
326    pub chunk: DataStream,
327    #[prost(enumeration = "PayloadType", tag = "3")]
328    pub payload_type: i32,
329}
330
331/// Payloads that can automatically construct runtime frames.
332pub trait AbiPayload: ProstMessage + Default + Sized {
333    const ABI_VERSION: u32;
334    const OP: u32;
335
336    fn to_frame(&self) -> Result<AbiFrame, i32> {
337        let mut payload = Vec::new();
338        self.encode(&mut payload)
339            .map_err(|_| code::PROTOCOL_ERROR)?;
340
341        Ok(AbiFrame {
342            abi_version: Self::ABI_VERSION,
343            op: Self::OP,
344            payload,
345        })
346    }
347
348    fn decode_payload(bytes: &[u8]) -> Result<Self, i32> {
349        Self::decode(bytes).map_err(|_| code::PROTOCOL_ERROR)
350    }
351}
352
353impl AbiPayload for HostCallV1 {
354    const ABI_VERSION: u32 = version::V1;
355    const OP: u32 = op::HOST_CALL;
356}
357
358impl AbiPayload for HostTellV1 {
359    const ABI_VERSION: u32 = version::V1;
360    const OP: u32 = op::HOST_TELL;
361}
362
363impl AbiPayload for HostCallRawV1 {
364    const ABI_VERSION: u32 = version::V1;
365    const OP: u32 = op::HOST_CALL_RAW;
366}
367
368impl AbiPayload for HostDiscoverV1 {
369    const ABI_VERSION: u32 = version::V1;
370    const OP: u32 = op::HOST_DISCOVER;
371}
372
373impl AbiPayload for HostRegisterStreamV1 {
374    const ABI_VERSION: u32 = version::V1;
375    const OP: u32 = op::HOST_REGISTER_STREAM;
376}
377
378impl AbiPayload for HostUnregisterStreamV1 {
379    const ABI_VERSION: u32 = version::V1;
380    const OP: u32 = op::HOST_UNREGISTER_STREAM;
381}
382
383impl AbiPayload for HostSendDataStreamV1 {
384    const ABI_VERSION: u32 = version::V1;
385    const OP: u32 = op::HOST_SEND_DATA_STREAM;
386}
387
388impl AbiPayload for GuestHandleV1 {
389    const ABI_VERSION: u32 = version::V1;
390    const OP: u32 = op::GUEST_HANDLE;
391}
392
393impl AbiPayload for GuestDataStreamV1 {
394    const ABI_VERSION: u32 = version::V1;
395    const OP: u32 = op::GUEST_DATA_STREAM;
396}
397
398impl AbiPayload for GuestLifecycleV1 {
399    const ABI_VERSION: u32 = version::V1;
400    const OP: u32 = op::GUEST_LIFECYCLE;
401}
402
403impl AbiPayload for GuestHookV1 {
404    const ABI_VERSION: u32 = version::V1;
405    const OP: u32 = op::GUEST_HOOK;
406}
407
408/// Encode a protobuf message into bytes.
409pub fn encode_message<M: ProstMessage>(message: &M) -> Result<Vec<u8>, i32> {
410    let mut out = Vec::new();
411    message.encode(&mut out).map_err(|_| code::PROTOCOL_ERROR)?;
412    Ok(out)
413}
414
415/// Decode a protobuf message from bytes.
416pub fn decode_message<M: ProstMessage + Default>(bytes: &[u8]) -> Result<M, i32> {
417    M::decode(bytes).map_err(|_| code::PROTOCOL_ERROR)
418}
419
420/// Encode a successful runtime reply.
421pub fn success_reply(payload: Vec<u8>) -> Result<Vec<u8>, i32> {
422    encode_message(&AbiReply {
423        abi_version: version::V1,
424        status: code::SUCCESS,
425        payload,
426    })
427}
428
429/// Encode a failed runtime reply.
430pub fn error_reply(status: i32, message: impl Into<Vec<u8>>) -> Result<Vec<u8>, i32> {
431    encode_message(&AbiReply {
432        abi_version: version::V1,
433        status,
434        payload: message.into(),
435    })
436}
437
438// ─────────────────────────────────────────────────────────────────────────────
439// Shared guest-side helpers (used by both WASM and DynClib contexts)
440// ─────────────────────────────────────────────────────────────────────────────
441
442/// Convert a [`crate::Dest`] to the ABI-level [`DestV1`].
443pub fn dest_to_v1(dest: &crate::Dest) -> DestV1 {
444    match dest {
445        crate::Dest::Shell => DestV1::shell(),
446        crate::Dest::Local => DestV1::local(),
447        crate::Dest::Actor(id) => DestV1::actor(id.clone()),
448    }
449}
450
451/// Convert an ABI-level [`DestV1`] back to [`crate::Dest`].
452///
453/// Returns `None` if the `kind` field is absent.
454pub fn dest_v1_to_dest(v1: &DestV1) -> Option<crate::Dest> {
455    v1.clone().try_into_dest().ok()
456}
457
458/// Convert an ABI error code to an [`actr_protocol::ActrError`].
459pub fn abi_error_to_actr(code_val: i32) -> actr_protocol::ActrError {
460    use actr_protocol::ActrError;
461    match code_val {
462        code::GENERIC_ERROR => ActrError::Internal("host returned generic ABI error".into()),
463        code::INIT_FAILED => ActrError::Internal("host initialization failed".into()),
464        code::HANDLE_FAILED => ActrError::Internal("guest handle failed".into()),
465        code::ALLOC_FAILED => ActrError::Internal("memory allocation failed".into()),
466        code::PROTOCOL_ERROR => ActrError::DecodeFailure("ABI payload decode failed".into()),
467        code::BUFFER_TOO_SMALL => {
468            ActrError::Internal("reply buffer too small for host invoke".into())
469        }
470        code::UNSUPPORTED_OP => ActrError::NotImplemented("unsupported ABI operation".into()),
471        other => ActrError::Internal(format!("unexpected ABI status code {other}")),
472    }
473}
474
475/// Convert an [`AbiReply`] with a non-success status to an [`actr_protocol::ActrError`].
476pub fn reply_to_actr_error(reply: AbiReply) -> actr_protocol::ActrError {
477    use actr_protocol::ActrError;
478    if reply.payload.is_empty() {
479        return abi_error_to_actr(reply.status);
480    }
481
482    let message = String::from_utf8(reply.payload)
483        .unwrap_or_else(|_| format!("guest returned status {}", reply.status));
484
485    match reply.status {
486        code::PROTOCOL_ERROR => ActrError::DecodeFailure(message),
487        code::UNSUPPORTED_OP => ActrError::NotImplemented(message),
488        _ => ActrError::Internal(message),
489    }
490}