convex-rust 0.0.0-stage → 0.1.0
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.
- package/LICENSE +201 -0
- package/README.md +137 -2
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +200 -0
- package/dist/cli/build.d.ts +146 -0
- package/dist/cli/build.js +902 -0
- package/dist/cli/demangle.d.ts +7 -0
- package/dist/cli/demangle.js +328 -0
- package/dist/cli/diagnostics.d.ts +34 -0
- package/dist/cli/diagnostics.js +296 -0
- package/dist/cli/generate.d.ts +29 -0
- package/dist/cli/generate.js +82 -0
- package/dist/cli/manifest.d.ts +24 -0
- package/dist/cli/manifest.js +75 -0
- package/dist/cli/rustApi.d.ts +78 -0
- package/dist/cli/rustApi.js +609 -0
- package/dist/cli/scaffold.d.ts +22 -0
- package/dist/cli/scaffold.js +114 -0
- package/dist/cli/shim.d.ts +30 -0
- package/dist/cli/shim.js +140 -0
- package/dist/cli/size.d.ts +17 -0
- package/dist/cli/size.js +73 -0
- package/dist/cli/split.d.ts +20 -0
- package/dist/cli/split.js +140 -0
- package/dist/cli/symbols.d.ts +30 -0
- package/dist/cli/symbols.js +133 -0
- package/dist/cli/toolchain.d.ts +53 -0
- package/dist/cli/toolchain.js +167 -0
- package/dist/cli/validators.d.ts +6 -0
- package/dist/cli/validators.js +66 -0
- package/dist/cli/wasisdk.d.ts +82 -0
- package/dist/cli/wasisdk.js +220 -0
- package/dist/cli/wasmHints.d.ts +29 -0
- package/dist/cli/wasmHints.js +172 -0
- package/dist/runtime/base64.d.ts +4 -0
- package/dist/runtime/base64.js +77 -0
- package/dist/runtime/crypto.d.ts +7 -0
- package/dist/runtime/crypto.js +421 -0
- package/dist/runtime/fastjson.d.ts +3 -0
- package/dist/runtime/fastjson.js +170 -0
- package/dist/runtime/host.d.ts +73 -0
- package/dist/runtime/host.js +1230 -0
- package/dist/runtime/http.d.ts +40 -0
- package/dist/runtime/http.js +157 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/intl.d.ts +5 -0
- package/dist/runtime/intl.js +69 -0
- package/dist/runtime/tmpfs.d.ts +129 -0
- package/dist/runtime/tmpfs.js +624 -0
- package/dist/runtime/wasi.d.ts +33 -0
- package/dist/runtime/wasi.js +216 -0
- package/package.json +57 -4
- package/rust/convex_fn/Cargo.toml +32 -0
- package/rust/convex_fn/LICENSE +201 -0
- package/rust/convex_fn/build.rs +3 -0
- package/rust/convex_fn/src/args.rs +416 -0
- package/rust/convex_fn/src/crypto.rs +237 -0
- package/rust/convex_fn/src/ctx.rs +1044 -0
- package/rust/convex_fn/src/dns.rs +356 -0
- package/rust/convex_fn/src/error.rs +179 -0
- package/rust/convex_fn/src/executor.rs +577 -0
- package/rust/convex_fn/src/fanout.rs +106 -0
- package/rust/convex_fn/src/fs.rs +19 -0
- package/rust/convex_fn/src/function_ref.rs +219 -0
- package/rust/convex_fn/src/http.rs +441 -0
- package/rust/convex_fn/src/intl.rs +75 -0
- package/rust/convex_fn/src/lib.rs +242 -0
- package/rust/convex_fn/src/logging.rs +222 -0
- package/rust/convex_fn/src/native.rs +360 -0
- package/rust/convex_fn/src/net.rs +122 -0
- package/rust/convex_fn/src/sys.rs +178 -0
- package/rust/convex_fn/src/values.rs +121 -0
- package/rust/convex_fn/src/wire.rs +579 -0
- package/rust/convex_fn_macros/Cargo.toml +13 -0
- package/rust/convex_fn_macros/LICENSE +201 -0
- package/rust/convex_fn_macros/src/derive.rs +632 -0
- package/rust/convex_fn_macros/src/lib.rs +338 -0
- package/rust/convex_reqwest/Cargo.toml +69 -0
- package/rust/convex_reqwest/LICENSE +201 -0
- package/rust/convex_reqwest/src/blocking.rs +287 -0
- package/rust/convex_reqwest/src/body.rs +152 -0
- package/rust/convex_reqwest/src/client.rs +359 -0
- package/rust/convex_reqwest/src/config_types.rs +150 -0
- package/rust/convex_reqwest/src/error.rs +241 -0
- package/rust/convex_reqwest/src/into_url.rs +57 -0
- package/rust/convex_reqwest/src/lib.rs +42 -0
- package/rust/convex_reqwest/src/multipart.rs +140 -0
- package/rust/convex_reqwest/src/request.rs +358 -0
- 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::*;
|