|
| 1 | +/* |
| 2 | + * Minimal wasm-side host of z_impl_k_msgq_put — the message-queue analogue of |
| 3 | + * sem_give_shim.c / mutex_unlock_shim.c. Replicates gale_msgq.c's put hot path |
| 4 | + * with the Zephyr kernel APIs as externs (which become wasm imports), so the |
| 5 | + * shim itself compiles to wasm32-unknown-unknown without pulling in Zephyr |
| 6 | + * headers. This puts the C <-> Rust seam (gale_k_msgq_put_decide) INSIDE the |
| 7 | + * wasm bundle: wasm-ld merges it with gale-ffi.wasm, loom inlines through it, |
| 8 | + * synth produces ARM with the seam dissolved (no `bl gale_k_msgq_put_decide`). |
| 9 | + * |
| 10 | + * Surface: z_impl_k_msgq_put (put hot path). k_msgq_get / init / cleanup stay |
| 11 | + * native (gale_msgq.c). All four decided actions are handled faithfully: |
| 12 | + * WAKE_READER — copy bytes into the waiting reader's buffer + wake |
| 13 | + * PUT_OK — copy bytes into the ring buffer at the write slot |
| 14 | + * RETURN_FULL — non-blocking full: return d.ret (-ENOMSG) |
| 15 | + * PUT_PEND — blocking full: delegate to gale_w_msgq_pend (native |
| 16 | + * z_pend_curr; the wait queue / scheduling stay native C, |
| 17 | + * exactly as docs/wasm-module-distribution.md prescribes). |
| 18 | + * |
| 19 | + * SHIM DISCIPLINE (default config: !SMP / !SPIN_VALIDATE / !NONZERO_SPINLOCK): |
| 20 | + * struct k_spinlock is a ZERO-size empty struct, so we OMIT the embedded `lock` |
| 21 | + * field (modelling it as sized would shift every later field and corrupt the |
| 22 | + * struct on store) and use a file-scope static spinlock. In this config every |
| 23 | + * k_spin_lock degenerates to arch_irq_lock(), so the shim's static lock and |
| 24 | + * gale_msgq.c's per-object lock are the SAME critical section — which is what |
| 25 | + * makes the pend(here)/wake(native get) handshake on msgq->wait_q safe across |
| 26 | + * the wasm-put / native-get boundary. Same constraint the sem/mutex modules |
| 27 | + * assume; not valid for SMP builds (those keep the native z_impl_k_msgq_put). |
| 28 | + * |
| 29 | + * ABI: the third argument k_timeout_t is a struct { k_ticks_t ticks } (one |
| 30 | + * 8-byte member), passed in r2:r3 under AAPCS — identical to a bare int64_t. |
| 31 | + * The shim therefore takes int64_t timeout_ticks (K_NO_WAIT.ticks == 0, |
| 32 | + * K_FOREVER.ticks == -1) and reconstructs k_timeout_t inside gale_w_msgq_pend. |
| 33 | + * |
| 34 | + * Faithful Zephyr v4.4.0 struct k_msgq field offsets (0-byte spinlock omitted, |
| 35 | + * WAITQ_DUMB): wait_q@0(8) | msg_size@8 | max_msgs@12 | buffer_start@16 | |
| 36 | + * buffer_end@20 | read_ptr@24 | write_ptr@28 | used_msgs@32. (Z_DECL_POLL_EVENT |
| 37 | + * / flags come AFTER used_msgs, so CONFIG_POLL doesn't affect any touched field; |
| 38 | + * poll-event handling on PUT_OK stays native — not exercised by this surface.) |
| 39 | + */ |
| 40 | + |
| 41 | +#include <stdint.h> |
| 42 | + |
| 43 | +/* Opaque k_thread — never deref'd in the shim; the kernel owns its layout. */ |
| 44 | +struct k_thread; |
| 45 | +struct k_spinlock { uint8_t lock_internal; }; /* type only; never embedded (0-byte real) */ |
| 46 | +typedef struct { uint32_t key; } k_spinlock_key_t; |
| 47 | + |
| 48 | +struct k_msgq { |
| 49 | + void *wq_head; /* @0 _wait_q_t wait_q (head,tail) */ |
| 50 | + void *wq_tail; /* @4 */ |
| 51 | + uint32_t msg_size; /* @8 (size_t on 32-bit) — 0-byte k_spinlock omitted */ |
| 52 | + uint32_t max_msgs; /* @12 */ |
| 53 | + char *buffer_start; /* @16 */ |
| 54 | + char *buffer_end; /* @20 */ |
| 55 | + char *read_ptr; /* @24 */ |
| 56 | + char *write_ptr; /* @28 */ |
| 57 | + uint32_t used_msgs; /* @32 */ |
| 58 | +}; |
| 59 | +static struct k_spinlock msgq_lock; |
| 60 | + |
| 61 | +/* Kernel API externs -> wasm imports -> native `bl` after synth-emit (renamed |
| 62 | + * to the gale_w_* wrappers by build-wasm-dist.sh's objcopy pass). */ |
| 63 | +extern k_spinlock_key_t k_spin_lock(struct k_spinlock *); |
| 64 | +extern void k_spin_unlock(struct k_spinlock *, k_spinlock_key_t); |
| 65 | +extern struct k_thread * z_unpend_first_thread(void *wait_q); |
| 66 | +extern void z_ready_thread(struct k_thread *); |
| 67 | +extern void arch_thread_return_value_set(struct k_thread *, uint32_t); |
| 68 | +extern int z_reschedule(struct k_spinlock *, k_spinlock_key_t); |
| 69 | +/* k_thread is opaque here: these out-of-line wrappers do what the native |
| 70 | + * z_impl_k_msgq_put reaches via struct fields / static helpers. */ |
| 71 | +extern void * gale_w_thread_swap_data(struct k_thread *); /* reader's dest buffer */ |
| 72 | +extern void gale_w_memcpy(void *dst, const void *src, uint32_t n); |
| 73 | +extern int gale_w_msgq_pend(void *wait_q, k_spinlock_key_t key, |
| 74 | + const void *data, int64_t timeout_ticks); |
| 75 | + |
| 76 | +/* #[repr(C)] GaleMsgqPutDecision — 16 bytes, returned by value (sret). */ |
| 77 | +struct gale_msgq_put_decision { int32_t ret; uint8_t action; uint32_t new_write_idx; uint32_t new_used; }; |
| 78 | +extern struct gale_msgq_put_decision gale_k_msgq_put_decide( |
| 79 | + uint32_t write_idx, uint32_t used_msgs, uint32_t max_msgs, |
| 80 | + uint32_t has_waiter, uint32_t is_no_wait); |
| 81 | + |
| 82 | +#define GALE_MSGQ_ACTION_PUT_OK 0 |
| 83 | +#define GALE_MSGQ_ACTION_WAKE_READER 1 |
| 84 | +#define GALE_MSGQ_ACTION_PUT_PEND 2 |
| 85 | +#define GALE_MSGQ_ACTION_RETURN_FULL 3 |
| 86 | + |
| 87 | +int z_impl_k_msgq_put(struct k_msgq *msgq, const void *data, int64_t timeout_ticks) |
| 88 | +{ |
| 89 | + uint32_t is_no_wait = (timeout_ticks == 0) ? 1U : 0U; /* K_NO_WAIT.ticks == 0 */ |
| 90 | + |
| 91 | + k_spinlock_key_t key = k_spin_lock(&msgq_lock); |
| 92 | + |
| 93 | + /* Extract: try to unpend first waiter (side effect: removes from queue). |
| 94 | + * wait_q is k_msgq's first member, so &msgq == &msgq->wait_q. */ |
| 95 | + struct k_thread *reader = z_unpend_first_thread((void *)msgq); |
| 96 | + |
| 97 | + uint32_t write_idx = (uint32_t)(msgq->write_ptr - msgq->buffer_start) / msgq->msg_size; |
| 98 | + |
| 99 | + struct gale_msgq_put_decision d = gale_k_msgq_put_decide( |
| 100 | + write_idx, msgq->used_msgs, msgq->max_msgs, |
| 101 | + reader != (struct k_thread *)0 ? 1U : 0U, is_no_wait); |
| 102 | + |
| 103 | + switch (d.action) { |
| 104 | + case GALE_MSGQ_ACTION_WAKE_READER: |
| 105 | + /* Receiver was waiting — copy the message into its buffer |
| 106 | + * (reader stashed its dest in swap_data before pending in get). */ |
| 107 | + gale_w_memcpy(gale_w_thread_swap_data(reader), data, msgq->msg_size); |
| 108 | + arch_thread_return_value_set(reader, 0U); |
| 109 | + z_ready_thread(reader); |
| 110 | + z_reschedule(&msgq_lock, key); |
| 111 | + return d.ret; |
| 112 | + case GALE_MSGQ_ACTION_PUT_OK: |
| 113 | + /* Space available — store at the write slot, advance ring. */ |
| 114 | + gale_w_memcpy(msgq->write_ptr, data, msgq->msg_size); |
| 115 | + msgq->write_ptr = msgq->buffer_start + (uint64_t)d.new_write_idx * msgq->msg_size; |
| 116 | + msgq->used_msgs = d.new_used; |
| 117 | + k_spin_unlock(&msgq_lock, key); |
| 118 | + return d.ret; |
| 119 | + case GALE_MSGQ_ACTION_PUT_PEND: |
| 120 | + /* Queue full, blocking — pend current thread natively. The |
| 121 | + * unpend above found no reader, so the wait_q is untouched. */ |
| 122 | + return gale_w_msgq_pend((void *)msgq, key, data, timeout_ticks); |
| 123 | + case GALE_MSGQ_ACTION_RETURN_FULL: |
| 124 | + default: |
| 125 | + k_spin_unlock(&msgq_lock, key); |
| 126 | + return d.ret; /* -ENOMSG */ |
| 127 | + } |
| 128 | +} |
0 commit comments