convex-rust 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +137 -2
  3. package/dist/cli/bin.d.ts +2 -0
  4. package/dist/cli/bin.js +200 -0
  5. package/dist/cli/build.d.ts +146 -0
  6. package/dist/cli/build.js +902 -0
  7. package/dist/cli/demangle.d.ts +7 -0
  8. package/dist/cli/demangle.js +328 -0
  9. package/dist/cli/diagnostics.d.ts +34 -0
  10. package/dist/cli/diagnostics.js +296 -0
  11. package/dist/cli/generate.d.ts +29 -0
  12. package/dist/cli/generate.js +82 -0
  13. package/dist/cli/manifest.d.ts +24 -0
  14. package/dist/cli/manifest.js +75 -0
  15. package/dist/cli/rustApi.d.ts +78 -0
  16. package/dist/cli/rustApi.js +609 -0
  17. package/dist/cli/scaffold.d.ts +22 -0
  18. package/dist/cli/scaffold.js +114 -0
  19. package/dist/cli/shim.d.ts +30 -0
  20. package/dist/cli/shim.js +140 -0
  21. package/dist/cli/size.d.ts +17 -0
  22. package/dist/cli/size.js +73 -0
  23. package/dist/cli/split.d.ts +20 -0
  24. package/dist/cli/split.js +140 -0
  25. package/dist/cli/symbols.d.ts +30 -0
  26. package/dist/cli/symbols.js +133 -0
  27. package/dist/cli/toolchain.d.ts +53 -0
  28. package/dist/cli/toolchain.js +167 -0
  29. package/dist/cli/validators.d.ts +6 -0
  30. package/dist/cli/validators.js +66 -0
  31. package/dist/cli/wasisdk.d.ts +82 -0
  32. package/dist/cli/wasisdk.js +220 -0
  33. package/dist/cli/wasmHints.d.ts +29 -0
  34. package/dist/cli/wasmHints.js +172 -0
  35. package/dist/runtime/base64.d.ts +4 -0
  36. package/dist/runtime/base64.js +77 -0
  37. package/dist/runtime/crypto.d.ts +7 -0
  38. package/dist/runtime/crypto.js +421 -0
  39. package/dist/runtime/fastjson.d.ts +3 -0
  40. package/dist/runtime/fastjson.js +170 -0
  41. package/dist/runtime/host.d.ts +73 -0
  42. package/dist/runtime/host.js +1230 -0
  43. package/dist/runtime/http.d.ts +40 -0
  44. package/dist/runtime/http.js +157 -0
  45. package/dist/runtime/index.d.ts +2 -0
  46. package/dist/runtime/index.js +1 -0
  47. package/dist/runtime/intl.d.ts +5 -0
  48. package/dist/runtime/intl.js +69 -0
  49. package/dist/runtime/tmpfs.d.ts +129 -0
  50. package/dist/runtime/tmpfs.js +624 -0
  51. package/dist/runtime/wasi.d.ts +33 -0
  52. package/dist/runtime/wasi.js +216 -0
  53. package/package.json +66 -4
  54. package/rust/convex_fn/Cargo.toml +32 -0
  55. package/rust/convex_fn/LICENSE +201 -0
  56. package/rust/convex_fn/build.rs +3 -0
  57. package/rust/convex_fn/src/args.rs +416 -0
  58. package/rust/convex_fn/src/crypto.rs +237 -0
  59. package/rust/convex_fn/src/ctx.rs +1044 -0
  60. package/rust/convex_fn/src/dns.rs +356 -0
  61. package/rust/convex_fn/src/error.rs +179 -0
  62. package/rust/convex_fn/src/executor.rs +577 -0
  63. package/rust/convex_fn/src/fanout.rs +106 -0
  64. package/rust/convex_fn/src/fs.rs +19 -0
  65. package/rust/convex_fn/src/function_ref.rs +219 -0
  66. package/rust/convex_fn/src/http.rs +441 -0
  67. package/rust/convex_fn/src/intl.rs +75 -0
  68. package/rust/convex_fn/src/lib.rs +242 -0
  69. package/rust/convex_fn/src/logging.rs +222 -0
  70. package/rust/convex_fn/src/native.rs +360 -0
  71. package/rust/convex_fn/src/net.rs +122 -0
  72. package/rust/convex_fn/src/sys.rs +178 -0
  73. package/rust/convex_fn/src/values.rs +121 -0
  74. package/rust/convex_fn/src/wire.rs +579 -0
  75. package/rust/convex_fn_macros/Cargo.toml +13 -0
  76. package/rust/convex_fn_macros/LICENSE +201 -0
  77. package/rust/convex_fn_macros/src/derive.rs +632 -0
  78. package/rust/convex_fn_macros/src/lib.rs +338 -0
  79. package/rust/convex_reqwest/Cargo.toml +69 -0
  80. package/rust/convex_reqwest/LICENSE +201 -0
  81. package/rust/convex_reqwest/src/blocking.rs +287 -0
  82. package/rust/convex_reqwest/src/body.rs +152 -0
  83. package/rust/convex_reqwest/src/client.rs +359 -0
  84. package/rust/convex_reqwest/src/config_types.rs +150 -0
  85. package/rust/convex_reqwest/src/error.rs +241 -0
  86. package/rust/convex_reqwest/src/into_url.rs +57 -0
  87. package/rust/convex_reqwest/src/lib.rs +42 -0
  88. package/rust/convex_reqwest/src/multipart.rs +140 -0
  89. package/rust/convex_reqwest/src/request.rs +358 -0
  90. package/rust/convex_reqwest/src/response.rs +310 -0
@@ -0,0 +1,360 @@
1
+ //! Native ABI v1 (contracts/NATIVE.md §2.4): the exports a `--cfg convex_native`
2
+ //! cdylib offers to the worker, and the host vtable it calls back through.
3
+
4
+ use std::alloc::{GlobalAlloc, Layout};
5
+ use std::cell::RefCell;
6
+ use std::future::Future;
7
+ use std::panic::{catch_unwind, AssertUnwindSafe};
8
+ use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
9
+
10
+ use serde::{de::DeserializeOwned, Serialize};
11
+
12
+ use crate::Ctx;
13
+
14
+ /// Native ABI version returned by `convex_native_init`.
15
+ pub const NATIVE_ABI_VERSION: u32 = 1;
16
+
17
+ /// Host vtable (contracts/NATIVE.md §2.4). Prefix-compatible growth via `size`.
18
+ #[repr(C)]
19
+ #[derive(Clone, Copy)]
20
+ pub struct HostVtableV1 {
21
+ pub size: usize,
22
+ pub version: u32,
23
+ pub task_submit: extern "C" fn(ptr: *const u8, len: usize) -> i64,
24
+ pub task_poll: extern "C" fn(block: i32, out_ptr: *mut *mut u8, out_len: *mut usize) -> i32,
25
+ pub sync_syscall: extern "C" fn(
26
+ op: *const u8,
27
+ op_len: usize,
28
+ args: *const u8,
29
+ args_len: usize,
30
+ out_ptr: *mut *mut u8,
31
+ out_len: *mut usize,
32
+ ) -> i32,
33
+ pub log: extern "C" fn(level: i32, ptr: *const u8, len: usize),
34
+ pub oom: extern "C" fn(limit: usize, requested: usize),
35
+ }
36
+
37
+ pub(crate) type Vt = HostVtableV1;
38
+
39
+ /// Result handle: `convex_run:*` returns a pointer to a boxed `ConvexBuf`.
40
+ #[repr(C)]
41
+ pub struct ConvexBuf {
42
+ pub ptr: *mut u8,
43
+ pub len: usize,
44
+ pub cap: usize,
45
+ }
46
+
47
+ static VTABLE: AtomicPtr<HostVtableV1> = AtomicPtr::new(std::ptr::null_mut());
48
+
49
+ pub(crate) fn vtable() -> Option<&'static Vt> {
50
+ // SAFETY: only ever set to a leaked, never-freed copy of the host's vtable.
51
+ unsafe { VTABLE.load(Ordering::Acquire).as_ref() }
52
+ }
53
+
54
+ /// Stores the host vtable (copied, so the host may free its own) and returns
55
+ /// the native ABI version, or 0 when the vtable is unusable.
56
+ ///
57
+ /// # Safety
58
+ /// `vt` must be null or point to a valid `HostVtableV1` of `(*vt).size` bytes.
59
+ #[no_mangle]
60
+ pub unsafe extern "C" fn convex_native_init(vt: *const HostVtableV1) -> u32 {
61
+ if vt.is_null() {
62
+ return 0;
63
+ }
64
+ let vt = &*vt;
65
+ if vt.size < std::mem::size_of::<HostVtableV1>() || vt.version != NATIVE_ABI_VERSION {
66
+ return 0;
67
+ }
68
+ let copy = Box::into_raw(Box::new(*vt));
69
+ VTABLE.store(copy, Ordering::Release);
70
+ NATIVE_ABI_VERSION
71
+ }
72
+
73
+ /// Sets the allocator limit in bytes (default: unlimited). Not in the frozen
74
+ /// vtable, so the worker calls this export after `convex_native_init`.
75
+ #[no_mangle]
76
+ pub extern "C" fn convex_native_set_memory_limit(limit: usize) {
77
+ LIMIT.store(limit, Ordering::Relaxed);
78
+ }
79
+
80
+ /// Optional idle hook (contracts/NATIVE.md §2.4), called by the worker on the
81
+ /// call thread after a call's reply was sent. `check_only != 0` returns 1 when
82
+ /// the last call's peak live bytes reached 8 MiB (a trim is worthwhile), else
83
+ /// 0, without touching the allocator. `check_only == 0` runs `mi_collect(true)`
84
+ /// so freed pages go back to the OS and returns 1. The worker checks after
85
+ /// every reply but collects only after ~100 ms without a new call, so
86
+ /// back-to-back calls neither wait for the trim nor re-fault purged pages.
87
+ /// mimalloc only purges while the allocator is active, so without this an
88
+ /// idle worker keeps its high-water memory. `mi_collect` acts on the calling
89
+ /// thread's heap (plus abandoned segments), which is where guest allocations
90
+ /// happen: the guest runs on the call thread.
91
+ #[no_mangle]
92
+ pub extern "C" fn convex_native_trim(check_only: i32) -> i32 {
93
+ if PEAK.load(Ordering::Relaxed) < TRIM_PEAK_BYTES {
94
+ return 0;
95
+ }
96
+ if check_only != 0 {
97
+ return 1;
98
+ }
99
+ flush_credit();
100
+ // SAFETY: mi_collect has no preconditions.
101
+ unsafe { libmimalloc_sys::mi_collect(true) };
102
+ 1
103
+ }
104
+
105
+ /// Same as the wasm export, with `usize` lengths.
106
+ #[no_mangle]
107
+ pub extern "C" fn convex_alloc(len: usize) -> *mut u8 {
108
+ let mut buf = Vec::<u8>::with_capacity(len.max(1));
109
+ let ptr = buf.as_mut_ptr();
110
+ std::mem::forget(buf);
111
+ ptr
112
+ }
113
+
114
+ /// Frees a handle returned by a `convex_run:*` export.
115
+ #[no_mangle]
116
+ pub extern "C" fn convex_free_result(handle: i64) {
117
+ if handle == 0 {
118
+ return;
119
+ }
120
+ // SAFETY: the handle came from `into_handle` and is freed once.
121
+ unsafe {
122
+ let b = Box::from_raw(handle as usize as *mut ConvexBuf);
123
+ drop(Vec::from_raw_parts(b.ptr, b.len, b.cap));
124
+ }
125
+ }
126
+
127
+ fn into_handle(bytes: String) -> i64 {
128
+ let mut v = std::mem::ManuallyDrop::new(bytes.into_bytes());
129
+ let buf = Box::new(ConvexBuf {
130
+ ptr: v.as_mut_ptr(),
131
+ len: v.len(),
132
+ cap: v.capacity(),
133
+ });
134
+ Box::into_raw(buf) as usize as i64
135
+ }
136
+
137
+ thread_local! {
138
+ static LAST_PANIC: RefCell<Option<String>> = const { RefCell::new(None) };
139
+ }
140
+
141
+ /// Called by the panic hook with `<msg> at <file:line:col>`.
142
+ pub(crate) fn record_panic(msg: String) {
143
+ LAST_PANIC.with(|p| *p.borrow_mut() = Some(msg));
144
+ }
145
+
146
+ /// Body of every native `convex_run:*` export. Never unwinds.
147
+ pub fn run_export_native<A, R, F, Fut>(args_ptr: *mut u8, args_len: usize, f: F) -> i64
148
+ where
149
+ A: DeserializeOwned,
150
+ R: Serialize,
151
+ F: FnOnce(Ctx, A) -> Fut,
152
+ Fut: Future<Output = crate::Result<R>>,
153
+ {
154
+ crate::error::install_panic_hook();
155
+ crate::executor::reset();
156
+ flush_credit();
157
+ LAST_PANIC.with(|p| p.borrow_mut().take());
158
+ let args = crate::sys::take_buf(args_ptr, args_len);
159
+ let envelope = match catch_unwind(AssertUnwindSafe(|| {
160
+ crate::__rt::run_to_envelope(&args, f)
161
+ })) {
162
+ Ok(env) => env,
163
+ Err(payload) => {
164
+ let message = LAST_PANIC
165
+ .with(|p| p.borrow_mut().take())
166
+ .or_else(|| {
167
+ payload
168
+ .downcast_ref::<&str>()
169
+ .map(|s| s.to_string())
170
+ .or_else(|| payload.downcast_ref::<String>().cloned())
171
+ })
172
+ .unwrap_or_else(|| "Box<dyn Any>".to_string());
173
+ serde_json::json!({ "error": { "message": message, "kind": "panic" } }).to_string()
174
+ }
175
+ };
176
+ into_handle(envelope)
177
+ }
178
+
179
+ // ---- counting allocator ---------------------------------------------------
180
+
181
+ use std::cell::Cell;
182
+
183
+ use mimalloc::MiMalloc;
184
+
185
+ /// Peak of `LIVE` since the last `flush_credit` (updated on the slow path
186
+ /// only, so it is a lower bound within one batch per thread).
187
+ static PEAK: AtomicUsize = AtomicUsize::new(0);
188
+ /// A call whose peak reached this many bytes makes `convex_native_trim`
189
+ /// collect the allocator (smaller calls leave the heap alone).
190
+ const TRIM_PEAK_BYTES: usize = 8 << 20;
191
+
192
+ /// Bytes a thread reserves from the shared counter at once.
193
+ const BATCH: usize = 256 * 1024;
194
+ /// A thread hands credit back once it holds more than this.
195
+ const MAX_CREDIT: usize = 2 * BATCH;
196
+
197
+ /// Bytes reserved against the limit (live bytes plus every thread's credit).
198
+ static LIVE: AtomicUsize = AtomicUsize::new(0);
199
+ static LIMIT: AtomicUsize = AtomicUsize::new(usize::MAX);
200
+
201
+ thread_local! {
202
+ // Reserved in `LIVE` but not in use by this thread. Const-init and no
203
+ // destructor, so it is safe to touch from inside the global allocator.
204
+ static CREDIT: Cell<usize> = const { Cell::new(0) };
205
+ }
206
+
207
+ /// Returns the calling thread's unused credit to the shared counter (called
208
+ /// at the start of each run so a long-lived worker does not drift).
209
+ pub(crate) fn flush_credit() {
210
+ PEAK.store(LIVE.load(Ordering::Relaxed), Ordering::Relaxed);
211
+ let _ = CREDIT.try_with(|c| {
212
+ let n = c.replace(0);
213
+ if n > 0 {
214
+ LIVE.fetch_sub(n, Ordering::Relaxed);
215
+ }
216
+ });
217
+ }
218
+
219
+ /// mimalloc with a per-call memory limit. Failing allocations are reported
220
+ /// through `vt.oom`.
221
+ ///
222
+ /// Accounting is batched per thread: a thread reserves `BATCH` (256 KiB) at a
223
+ /// time from `LIVE` and serves small allocations from that credit without
224
+ /// touching a shared atomic; frees refill the credit and anything above
225
+ /// `MAX_CREDIT` (512 KiB) is returned. Allocations of `BATCH` or more go to
226
+ /// `LIVE` exactly. The limit check is therefore exact for large allocations
227
+ /// and conservative otherwise: an allocation can fail up to `MAX_CREDIT` per
228
+ /// thread (<= 512 KiB, ~1 MiB with the two threads a guest normally has)
229
+ /// before the true live size reaches the limit, but never succeeds beyond it.
230
+ /// A thread that exits holding credit leaks at most `MAX_CREDIT` until the
231
+ /// worker process is recycled.
232
+ ///
233
+ /// mimalloc keeps its default purge delay: `purge_delay = 0` was measured
234
+ /// (macOS, L3-01) to double the time of allocation-heavy calls without
235
+ /// lowering worker RSS (freed pages stay resident under MADV_FREE).
236
+ pub struct CountingAlloc;
237
+
238
+ impl CountingAlloc {
239
+ /// Adds `n` to `LIVE` if that stays within the limit.
240
+ fn take_global(n: usize) -> bool {
241
+ let limit = LIMIT.load(Ordering::Relaxed);
242
+ let live = LIVE.fetch_add(n, Ordering::Relaxed);
243
+ if live.saturating_add(n) > limit {
244
+ LIVE.fetch_sub(n, Ordering::Relaxed);
245
+ return false;
246
+ }
247
+ PEAK.fetch_max(live.saturating_add(n), Ordering::Relaxed);
248
+ true
249
+ }
250
+
251
+ #[cold]
252
+ fn reserve_slow(extra: usize) -> bool {
253
+ // Try a whole batch first (small requests only), then the exact size.
254
+ if extra < BATCH && Self::take_global(BATCH) {
255
+ let _ = CREDIT.try_with(|c| c.set(c.get() + BATCH - extra));
256
+ return true;
257
+ }
258
+ if Self::take_global(extra) {
259
+ return true;
260
+ }
261
+ if let Some(vt) = vtable() {
262
+ (vt.oom)(LIMIT.load(Ordering::Relaxed), extra);
263
+ }
264
+ false
265
+ }
266
+
267
+ /// Reserves `extra` bytes against the limit; false (after reporting) if over.
268
+ #[inline]
269
+ fn reserve(extra: usize) -> bool {
270
+ if extra < BATCH {
271
+ let hit = CREDIT
272
+ .try_with(|c| {
273
+ let have = c.get();
274
+ if have >= extra {
275
+ c.set(have - extra);
276
+ true
277
+ } else {
278
+ false
279
+ }
280
+ })
281
+ .unwrap_or(false);
282
+ if hit {
283
+ return true;
284
+ }
285
+ }
286
+ Self::reserve_slow(extra)
287
+ }
288
+
289
+ /// Gives `n` bytes back.
290
+ #[inline]
291
+ fn release(n: usize) {
292
+ let done = CREDIT
293
+ .try_with(|c| {
294
+ let have = c.get() + n;
295
+ if have > MAX_CREDIT {
296
+ c.set(BATCH);
297
+ LIVE.fetch_sub(have - BATCH, Ordering::Relaxed);
298
+ } else {
299
+ c.set(have);
300
+ }
301
+ })
302
+ .is_ok();
303
+ if !done {
304
+ LIVE.fetch_sub(n, Ordering::Relaxed);
305
+ }
306
+ }
307
+ }
308
+
309
+ // SAFETY: delegates to mimalloc; counters are atomics/thread-locals and never allocate.
310
+ unsafe impl GlobalAlloc for CountingAlloc {
311
+ #[inline]
312
+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
313
+ if !Self::reserve(layout.size()) {
314
+ return std::ptr::null_mut();
315
+ }
316
+ let p = MiMalloc.alloc(layout);
317
+ if p.is_null() {
318
+ Self::release(layout.size());
319
+ }
320
+ p
321
+ }
322
+
323
+ #[inline]
324
+ unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
325
+ if !Self::reserve(layout.size()) {
326
+ return std::ptr::null_mut();
327
+ }
328
+ let p = MiMalloc.alloc_zeroed(layout);
329
+ if p.is_null() {
330
+ Self::release(layout.size());
331
+ }
332
+ p
333
+ }
334
+
335
+ #[inline]
336
+ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
337
+ MiMalloc.dealloc(ptr, layout);
338
+ Self::release(layout.size());
339
+ }
340
+
341
+ #[inline]
342
+ unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
343
+ let old = layout.size();
344
+ if new_size > old && !Self::reserve(new_size - old) {
345
+ return std::ptr::null_mut();
346
+ }
347
+ let p = MiMalloc.realloc(ptr, layout, new_size);
348
+ if p.is_null() {
349
+ if new_size > old {
350
+ Self::release(new_size - old);
351
+ }
352
+ } else if new_size < old {
353
+ Self::release(old - new_size);
354
+ }
355
+ p
356
+ }
357
+ }
358
+
359
+ #[global_allocator]
360
+ static ALLOC: CountingAlloc = CountingAlloc;
@@ -0,0 +1,122 @@
1
+ //! Optional `net/*` sync ops (contracts/ABI.md "Optional sync ops: `net/*`"):
2
+ //! task / stream cancellation, `{ptr,len}` request bodies and guest-fed
3
+ //! (streamed) request bodies.
4
+ //!
5
+ //! The ops ride on `sync_syscall` (no new wasm import). The guest probes
6
+ //! `net/caps` once per instance; a host without them keeps the older
7
+ //! behaviour (inline base64 bodies, no cancellation, a streamed upload is
8
+ //! buffered), so a module still works on an older host.
9
+
10
+ use std::cell::Cell;
11
+
12
+ use serde_json::{json, Value};
13
+
14
+ use crate::{error::Error, Result};
15
+
16
+ /// Request bodies at least this large go as `{ptr,len}` references.
17
+ pub(crate) const REF_THRESHOLD: usize = 8 * 1024;
18
+ /// Size of the slices a body bigger than the host's single-buffer cap is cut into.
19
+ pub(crate) const STREAM_SLICE: usize = 1024 * 1024;
20
+
21
+ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22
+ pub(crate) struct Caps {
23
+ pub cancel: bool,
24
+ pub body_ref: bool,
25
+ pub request_stream: bool,
26
+ /// Largest `{ptr,len}` body one task may carry.
27
+ pub max_body_ref: usize,
28
+ }
29
+
30
+ thread_local! {
31
+ static CAPS: Cell<Option<Caps>> = const { Cell::new(None) };
32
+ }
33
+
34
+ pub(crate) fn reset() {
35
+ CAPS.with(|c| c.set(None));
36
+ }
37
+
38
+ /// The host's `net/*` capabilities, probed on first use (per instance).
39
+ pub(crate) fn caps() -> Caps {
40
+ if let Some(c) = CAPS.with(Cell::get) {
41
+ return c;
42
+ }
43
+ let c = probe();
44
+ CAPS.with(|cell| cell.set(Some(c)));
45
+ c
46
+ }
47
+
48
+ /// The capabilities if they were already probed; never calls the host.
49
+ pub(crate) fn known() -> Caps {
50
+ CAPS.with(Cell::get).unwrap_or_default()
51
+ }
52
+
53
+ fn probe() -> Caps {
54
+ let Ok(v) = call("net/caps", json!({ "version": 1 })) else {
55
+ return Caps::default();
56
+ };
57
+ let has = |op: &str| {
58
+ v.get("ops")
59
+ .and_then(Value::as_array)
60
+ .is_some_and(|a| a.iter().any(|x| x.as_str() == Some(op)))
61
+ };
62
+ Caps {
63
+ cancel: has("cancel"),
64
+ body_ref: has("bodyRef"),
65
+ request_stream: has("requestStream"),
66
+ max_body_ref: v
67
+ .get("maxBodyRef")
68
+ .and_then(Value::as_u64)
69
+ .map_or(64 << 20, |n| n as usize),
70
+ }
71
+ }
72
+
73
+ fn call(op: &str, args: Value) -> Result<Value> {
74
+ let out = crate::sys::sync(op, args.to_string().as_bytes());
75
+ let v: Value = serde_json::from_slice(&out)?;
76
+ match v.get("error") {
77
+ Some(e) => Err(Error::from_json(e)),
78
+ None => Ok(v.get("ok").cloned().unwrap_or(Value::Null)),
79
+ }
80
+ }
81
+
82
+ /// A byte slice as a guest-memory reference.
83
+ pub(crate) fn bytes_ref(b: &[u8]) -> Value {
84
+ json!({ "ptr": b.as_ptr() as usize, "len": b.len() })
85
+ }
86
+
87
+ /// Aborts the host work of a task (its fetch, and a streamed request body).
88
+ /// Best effort; only called for tasks submitted under `cancel` support.
89
+ pub(crate) fn cancel_task(id: i64) {
90
+ let _ = call("net/cancel", json!({ "taskId": id }));
91
+ }
92
+
93
+ /// Aborts a body stream the guest stopped reading (the rest of the download).
94
+ pub(crate) fn cancel_stream(stream_id: &str) {
95
+ if known().cancel {
96
+ let _ = call("net/cancel", json!({ "streamId": stream_id }));
97
+ }
98
+ }
99
+
100
+ /// Appends a chunk to the streamed request body of task `id`. The host copies
101
+ /// the bytes and, when its buffer is full, suspends the guest until the
102
+ /// network has drained it (backpressure).
103
+ pub(crate) fn body_write(id: i64, data: &[u8]) -> Result<()> {
104
+ call(
105
+ "net/body_write",
106
+ json!({ "taskId": id, "data": bytes_ref(data) }),
107
+ )
108
+ .map(|_| ())
109
+ }
110
+
111
+ /// Ends the streamed request body of task `id`.
112
+ pub(crate) fn body_end(id: i64) -> Result<()> {
113
+ call("net/body_end", json!({ "taskId": id })).map(|_| ())
114
+ }
115
+
116
+ /// Fails the streamed request body of task `id`, which fails the request.
117
+ pub(crate) fn body_abort(id: i64, message: &str) {
118
+ let _ = call(
119
+ "net/body_abort",
120
+ json!({ "taskId": id, "message": message }),
121
+ );
122
+ }
@@ -0,0 +1,178 @@
1
+ //! The four host imports of contracts/ABI.md, plus a native stand-in so the
2
+ //! same code runs under `cargo test` against `convex_fn_test`.
3
+
4
+ use crate::wire::LogLevel;
5
+
6
+ #[cfg(target_arch = "wasm32")]
7
+ mod imp {
8
+ use super::LogLevel;
9
+
10
+ #[link(wasm_import_module = "convex")]
11
+ extern "C" {
12
+ fn task_submit(ptr: *const u8, len: usize) -> i64;
13
+ fn task_poll(block: i32) -> i64;
14
+ fn sync_syscall(
15
+ op_ptr: *const u8,
16
+ op_len: usize,
17
+ args_ptr: *const u8,
18
+ args_len: usize,
19
+ ) -> i64;
20
+ fn log(level: i32, ptr: *const u8, len: usize);
21
+ }
22
+
23
+ /// Host-side allocation entry point (contracts/ABI.md "Memory protocol").
24
+ #[no_mangle]
25
+ pub extern "C" fn convex_alloc(len: usize) -> *mut u8 {
26
+ let mut buf = Vec::<u8>::with_capacity(len.max(1));
27
+ let ptr = buf.as_mut_ptr();
28
+ std::mem::forget(buf);
29
+ ptr
30
+ }
31
+
32
+ /// Takes ownership of a packed `(ptr << 32) | len` buffer written by the host.
33
+ pub(crate) fn take_packed(packed: i64) -> Vec<u8> {
34
+ let ptr = (packed as u64 >> 32) as usize as *mut u8;
35
+ let len = (packed as u64 & 0xffff_ffff) as usize;
36
+ // SAFETY: the host allocated `len` bytes via `convex_alloc(len)`, which
37
+ // created a Vec with capacity `len.max(1)`, and handed it to us once.
38
+ unsafe { Vec::from_raw_parts(ptr, len, len.max(1)) }
39
+ }
40
+
41
+ pub fn submit(req: &[u8]) -> i64 {
42
+ unsafe { task_submit(req.as_ptr(), req.len()) }
43
+ }
44
+
45
+ pub fn poll(block: bool) -> Option<Vec<u8>> {
46
+ let packed = unsafe { task_poll(block as i32) };
47
+ // 0 = nothing ready, -1 = no tasks outstanding. A pointer >= 2 GiB makes `packed` negative, so
48
+ // only these two values mean "no event".
49
+ (packed != 0 && packed != -1).then(|| take_packed(packed))
50
+ }
51
+
52
+ pub fn sync(op: &str, args: &[u8]) -> Vec<u8> {
53
+ let packed = unsafe { sync_syscall(op.as_ptr(), op.len(), args.as_ptr(), args.len()) };
54
+ take_packed(packed)
55
+ }
56
+
57
+ pub fn log_line(level: LogLevel, msg: &str) {
58
+ unsafe { log(level.as_i32(), msg.as_ptr(), msg.len()) }
59
+ }
60
+
61
+ /// Presence of this export tells the host that a real panic is confirmed by a level-4 `log`
62
+ /// call right after the `Uncaught panic: ` line, so a user log line with that prefix is not
63
+ /// mistaken for one. Hosts that do not know it keep using the prefix line alone.
64
+ #[no_mangle]
65
+ pub extern "C" fn convex_panic_marker() -> i32 {
66
+ 1
67
+ }
68
+
69
+ /// Confirms the panic line just logged (level 4, empty message).
70
+ pub fn panic_marker() {
71
+ unsafe { log(4, "".as_ptr(), 0) }
72
+ }
73
+ }
74
+
75
+ #[cfg(all(not(target_arch = "wasm32"), convex_native))]
76
+ mod imp {
77
+ use super::LogLevel;
78
+ use crate::native::{vtable, Vt};
79
+
80
+ /// Takes ownership of a buffer the host allocated with `convex_alloc(len)`.
81
+ pub(crate) fn take_buf(ptr: *mut u8, len: usize) -> Vec<u8> {
82
+ if ptr.is_null() {
83
+ return Vec::new();
84
+ }
85
+ // SAFETY: `convex_alloc(len)` created a Vec with capacity `len.max(1)`
86
+ // and the host handed it to us exactly once.
87
+ unsafe { Vec::from_raw_parts(ptr, len, len.max(1)) }
88
+ }
89
+
90
+ fn vt() -> &'static Vt {
91
+ vtable().expect("convex_native_init has not been called")
92
+ }
93
+
94
+ pub fn submit(req: &[u8]) -> i64 {
95
+ (vt().task_submit)(req.as_ptr(), req.len())
96
+ }
97
+
98
+ pub fn poll(block: bool) -> Option<Vec<u8>> {
99
+ let mut ptr = std::ptr::null_mut();
100
+ let mut len = 0usize;
101
+ let r = (vt().task_poll)(block as i32, &mut ptr, &mut len);
102
+ (r == 1).then(|| take_buf(ptr, len))
103
+ }
104
+
105
+ pub fn sync(op: &str, args: &[u8]) -> Vec<u8> {
106
+ let mut ptr = std::ptr::null_mut();
107
+ let mut len = 0usize;
108
+ (vt().sync_syscall)(
109
+ op.as_ptr(),
110
+ op.len(),
111
+ args.as_ptr(),
112
+ args.len(),
113
+ &mut ptr,
114
+ &mut len,
115
+ );
116
+ take_buf(ptr, len)
117
+ }
118
+
119
+ pub fn log_line(level: LogLevel, msg: &str) {
120
+ match vtable() {
121
+ Some(vt) => (vt.log)(level.as_i32(), msg.as_ptr(), msg.len()),
122
+ None => eprintln!("{msg}"),
123
+ }
124
+ }
125
+
126
+ pub fn panic_marker() {}
127
+ }
128
+
129
+ #[cfg(all(not(target_arch = "wasm32"), not(convex_native)))]
130
+ mod imp {
131
+ use std::cell::RefCell;
132
+
133
+ use super::LogLevel;
134
+ use crate::testing::Host;
135
+
136
+ thread_local! {
137
+ static HOST: RefCell<Option<Box<dyn Host>>> = const { RefCell::new(None) };
138
+ }
139
+
140
+ pub(crate) fn install(host: Box<dyn Host>) -> Option<Box<dyn Host>> {
141
+ HOST.with(|h| h.borrow_mut().replace(host))
142
+ }
143
+
144
+ pub(crate) fn uninstall() -> Option<Box<dyn Host>> {
145
+ HOST.with(|h| h.borrow_mut().take())
146
+ }
147
+
148
+ fn with_host<R>(f: impl FnOnce(&mut dyn Host) -> R) -> R {
149
+ HOST.with(|h| {
150
+ let mut h = h.borrow_mut();
151
+ let host = h.as_deref_mut().expect(
152
+ "no Convex host installed: use convex_fn_test::TestHost to run actions natively",
153
+ );
154
+ f(host)
155
+ })
156
+ }
157
+
158
+ pub fn submit(req: &[u8]) -> i64 {
159
+ with_host(|h| h.task_submit(req))
160
+ }
161
+
162
+ pub fn poll(block: bool) -> Option<Vec<u8>> {
163
+ with_host(|h| h.task_poll(block))
164
+ }
165
+
166
+ pub fn sync(op: &str, args: &[u8]) -> Vec<u8> {
167
+ with_host(|h| h.sync_syscall(op, args))
168
+ }
169
+
170
+ pub fn log_line(level: LogLevel, msg: &str) {
171
+ HOST.with(|h| match h.borrow_mut().as_deref_mut() {
172
+ Some(host) => host.log(level.as_i32(), msg),
173
+ None => eprintln!("{msg}"),
174
+ })
175
+ }
176
+ }
177
+
178
+ pub(crate) use imp::*;