-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathapi.rs
More file actions
290 lines (262 loc) · 11.8 KB
/
Copy pathapi.rs
File metadata and controls
290 lines (262 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
use std::os::raw::{c_char, c_int, c_void};
use std::slice;
use std::time::Duration;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::ffi;
use super::types::*;
fn encode_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
let mut buf = Vec::new();
ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?;
Ok(buf)
}
fn decode_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, String> {
ciborium::de::from_reader(bytes).map_err(|e| e.to_string())
}
type FFIResult = Result<Vec<u8>, String>;
type FFISender = flume::Sender<FFIResult>;
// Reconstruct the (ret, msg, len) tuple delivered by the C callback
// into a Result<Vec<u8>, String>: payload on success, UTF-8 message on error.
// `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the
// alternative would be to dispatch a separate Err for invalid UTF-8, but the
// codegen contract is that Nim handlers emit `string` error payloads, so
// invalid UTF-8 here would be a Nim-side bug.
unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult {
let bytes = if msg.is_null() || len == 0 {
Vec::new()
} else {
slice::from_raw_parts(msg as *const u8, len).to_vec()
};
if ret == 0 { Ok(bytes) }
else { Err(String::from_utf8_lossy(&bytes).into_owned()) }
}
unsafe extern "C" fn on_result(
ret: c_int,
msg: *const c_char,
len: usize,
user_data: *mut c_void,
) {
// Take ownership of the boxed Sender — dropping it at end of scope
// releases the only outstanding handle.
let tx = Box::from_raw(user_data as *mut FFISender);
// `tx.send` returns Err only if the awaiting future was dropped (and with it
// the Receiver): e.g. tokio::time::timeout elapsed, a tokio::select! branch
// lost the race, or the future was dropped before being awaited. This cannot
// happen with the current rust_client demo but may occur in arbitrary
// downstream consumers, so we discard the Err safely.
// Given that this is invoked from a Nim thread, we can't propagate the error by panicking or
// returning a Result. Furthermore, an API dev may intentionally set a timeout in the await,
// in which case is also fine to discard the send error in this case because the API user will
// handle the timeout expiry in their own code.
// The important part is to ensure that the callback doesn't panic or block indefinitely if the
// receiver is gone.
let _ = tx.send(ffi_payload(ret, msg, len));
}
fn ffi_call_sync<F>(timeout: Duration, f: F) -> FFIResult
where
F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,
{
let (tx, rx) = flume::bounded::<FFIResult>(1);
let raw = Box::into_raw(Box::new(tx)) as *mut c_void;
let ret = f(on_result, raw);
if ret == 2 {
// Callback will never fire; reclaim the box to avoid a leak.
drop(unsafe { Box::from_raw(raw as *mut FFISender) });
return Err("RET_MISSING_CALLBACK (internal error)".into());
}
match rx.recv_timeout(timeout) {
Ok(payload) => payload,
Err(flume::RecvTimeoutError::Timeout) =>
Err(format!("timed out after {:?}", timeout)),
Err(flume::RecvTimeoutError::Disconnected) =>
Err("callback channel disconnected before delivery".into()),
}
}
async fn ffi_call_async<F>(timeout: Duration, f: F) -> FFIResult
where
F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,
{
let (tx, rx) = flume::bounded::<FFIResult>(1);
let raw = Box::into_raw(Box::new(tx)) as *mut c_void;
let ret = f(on_result, raw);
if ret == 2 {
drop(unsafe { Box::from_raw(raw as *mut FFISender) });
return Err("RET_MISSING_CALLBACK (internal error)".into());
}
match tokio::time::timeout(timeout, rx.recv_async()).await {
Ok(Ok(payload)) => payload,
Ok(Err(_)) => Err("callback channel disconnected before delivery".into()),
Err(_) => Err(format!("timed out after {:?}", timeout)),
}
}
struct OnEchoFiredHandler {
f: Box<dyn Fn(&EchoEvent) + Send + Sync>,
}
unsafe extern "C" fn on_echo_fired_trampoline(
ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,
) {
if ud.is_null() || ret != 0 || msg.is_null() || len == 0 {
return;
}
let h = &*(ud as *const OnEchoFiredHandler);
let bytes = slice::from_raw_parts(msg as *const u8, len);
#[derive(serde::Deserialize)]
struct Envelope { payload: EchoEvent }
if let Ok(env) = ciborium::de::from_reader::<Envelope, _>(bytes) {
(h.f)(&env.payload);
}
}
#[derive(Debug, Clone, Copy)]
pub struct ListenerHandle { pub id: u64 }
/// High-level context for `MyTimer`.
pub struct MyTimerCtx {
ptr: *mut c_void,
timeout: Duration,
listeners: std::sync::Mutex<std::collections::HashMap<u64, Box<dyn std::any::Any + Send>>>,
}
// SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime.
// Every call through the generated FFI proc goes through
// `sendRequestToFFIThread` on the Nim side, which serialises every request
// behind `ctx.lock` and dispatches handlers on a single FFI thread, so the
// pointer is never accessed concurrently from Rust. The Nim-side reentrancy
// guard (`onFFIThread` threadvar) prevents handlers from re-entering the
// dispatcher and self-deadlocking. These invariants make it sound to mark
// the wrapper as Send + Sync.
unsafe impl Send for MyTimerCtx {}
unsafe impl Sync for MyTimerCtx {}
impl Drop for MyTimerCtx {
fn drop(&mut self) {
if !self.ptr.is_null() {
let _ = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::my_timer_destroy(self.ptr, cb, ud)
});
self.ptr = std::ptr::null_mut();
}
}
}
impl MyTimerCtx {
pub fn create(config: TimerConfig, timeout: Duration) -> Result<Self, String> {
let req = MyTimerCreateCtorReq { config };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe {
let _ = ffi::my_timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud);
0
})?;
let addr_str: String = decode_cbor(&raw_bytes)?;
let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })
}
pub async fn new_async(config: TimerConfig, timeout: Duration) -> Result<Self, String> {
let req = MyTimerCreateCtorReq { config };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe {
let _ = ffi::my_timer_create(req_bytes.as_ptr(), req_bytes.len(), cb, ud);
0
}).await?;
let addr_str: String = decode_cbor(&raw_bytes)?;
let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })
}
fn add_listener_inner(
&self,
event_name: *const c_char,
callback: ffi::FFICallback,
raw: *mut c_void,
owned: Box<dyn std::any::Any + Send>,
) -> ListenerHandle {
let id = unsafe {
ffi::my_timer_add_event_listener(self.ptr, event_name, callback, raw)
};
if id != 0 {
self.listeners.lock().unwrap().insert(id, owned);
}
ListenerHandle { id }
}
/// Register a typed listener for `on_echo_fired`. The returned handle can be
/// passed to `remove_event_listener` to unregister.
pub fn add_on_echo_fired_listener<F>(&self, handler: F) -> ListenerHandle
where F: Fn(&EchoEvent) + Send + Sync + 'static,
{
let owned: Box<OnEchoFiredHandler> = Box::new(OnEchoFiredHandler { f: Box::new(handler) });
let raw = &*owned as *const OnEchoFiredHandler as *mut c_void;
self.add_listener_inner(b"on_echo_fired\0".as_ptr() as *const c_char, on_echo_fired_trampoline, raw, owned)
}
/// Remove a previously-registered listener by handle. Returns true
/// if the listener existed and was removed; false otherwise.
pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {
if handle.id == 0 { return false; }
let rc = unsafe {
ffi::my_timer_remove_event_listener(self.ptr, handle.id)
};
self.listeners.lock().unwrap().remove(&handle.id);
rc == 0
}
pub fn echo(&self, req: EchoRequest) -> Result<EchoResponse, String> {
let req = MyTimerEchoReq { req };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::my_timer_echo(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())
})?;
decode_cbor::<EchoResponse>(&raw_bytes)
}
pub async fn echo_async(&self, req: EchoRequest) -> Result<EchoResponse, String> {
let req = MyTimerEchoReq { req };
let req_bytes = encode_cbor(&req)?;
let ptr = self.ptr as usize;
let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {
ffi::my_timer_echo(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())
}).await?;
decode_cbor::<EchoResponse>(&raw_bytes)
}
pub fn version(&self) -> Result<String, String> {
let req = MyTimerVersionReq {};
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::my_timer_version(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())
})?;
decode_cbor::<String>(&raw_bytes)
}
pub async fn version_async(&self) -> Result<String, String> {
let req = MyTimerVersionReq {};
let req_bytes = encode_cbor(&req)?;
let ptr = self.ptr as usize;
let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {
ffi::my_timer_version(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())
}).await?;
decode_cbor::<String>(&raw_bytes)
}
pub fn complex(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
let req = MyTimerComplexReq { req };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::my_timer_complex(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())
})?;
decode_cbor::<ComplexResponse>(&raw_bytes)
}
pub async fn complex_async(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
let req = MyTimerComplexReq { req };
let req_bytes = encode_cbor(&req)?;
let ptr = self.ptr as usize;
let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {
ffi::my_timer_complex(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())
}).await?;
decode_cbor::<ComplexResponse>(&raw_bytes)
}
pub fn schedule(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
let req = MyTimerScheduleReq { job, retry, schedule };
let req_bytes = encode_cbor(&req)?;
let raw_bytes = ffi_call_sync(self.timeout, |cb, ud| unsafe {
ffi::my_timer_schedule(self.ptr, cb, ud, req_bytes.as_ptr(), req_bytes.len())
})?;
decode_cbor::<ScheduleResult>(&raw_bytes)
}
pub async fn schedule_async(&self, job: JobSpec, retry: RetryPolicy, schedule: ScheduleConfig) -> Result<ScheduleResult, String> {
let req = MyTimerScheduleReq { job, retry, schedule };
let req_bytes = encode_cbor(&req)?;
let ptr = self.ptr as usize;
let raw_bytes = ffi_call_async(self.timeout, move |cb, ud| unsafe {
ffi::my_timer_schedule(ptr as *mut c_void, cb, ud, req_bytes.as_ptr(), req_bytes.len())
}).await?;
decode_cbor::<ScheduleResult>(&raw_bytes)
}
}