random
saul
We can probably not have SAUL devices implemented in VMs for two reasons:
coap
trigger-me-an-event-in-this-and-that-time
puts
system information callback (get board name)
vfs
Let’s not do interrupt-style things in VMs – one VM with one stack always runs one routine to completion.
Options for events:
main routine sets some pause-flag, which events then resume (“WFI style”, maybe with a whiff of thread flags).
The interrupts would be level triggered, and it’d be up to the application inside the VM to find which event is to be processed (doing which clears the interrupt).
main routine only registers callbacks to events (unless that’s done by static configuration anyway), and after it has completed, any routine may run (“everything-is-an-interrupt style”)
(I somehow like this one better … especially if we can ensure users don’t need globals to write meaningful programs.)
Can programs, rather than being just-code-and-please-jump-right-into-main, be described with all the handles they need, and and callbacks set in the description?
A simple example:
https://chaos.social/@overflo/110204374724327082
(This might even be self-explanatory: If the program gets uploaded to a “run our LEDs like this” endpoint, it’d see that there is a render
and beforeRender
and act accordingly)
A complex example:
This program wants to have these handles:
time
)T_surf
“surface temp”, T_air
“air temp”)RH_air
“air humidity”)last_switch
)out
“emergency heating”)When any input changes, run trigger(time, T_surf, T_air, RH_air, last_switch, out)
:
fn trigger(time, T_surf, T_air, RH_air, last_switch, out) {
const HYSTERESIS = 10;
if time - HYSTERESIS < last_switch {
return;
}
dew_point = calculate_dew_poin(T_air, RH_air);
if T_surf < dew_point && !out {
last_switch = time;
out = true;
}
if T_surf > dw_point && out {
last_switch = time;
out = false;
}
}
(Depending on how the reads and writes are implemented, this may get away with no syscalls at all. But to not calculate once per second, it’d be better to keep reconfiguring the time event.)
By the way, this should work for CoAP just as for SAUL ;-)
(and let’s replace this with a any established syntax if there is one)
VMs may treat any Handler just like an integer; it’s only the backend implementations that’ll need to deal with that
// These look like Rust signatures, and indeed that's not a coincidence.
// Handle<T> would be Copy+Clone, and interiorly mutable. (Whether a
// function reads, writes or even consumes a handle is not described in
// this idiom, although it may make sense to add that metadata, even if
// most languages would still discard the information and just pass the
// handle)
random_u16() -> u16
random_u32() -> u32
fill_random_buffer(buffer: &mut [u8])
// For some reason I'm assuming here that we're more often need the
// full list of SAUL devices, and less often the saul_reg_find_* --
// but both could be wrapped.
saul_iter_init() -> Handler<saul reg>
saul_iter_next(Handler<saul reg>)
handler_is_valid(Handler<T>) -> bool
saul_get_type(Handler<saul reg>) -> u16
saul_read_name(Handler<saul reg>, buffer: &mut [u8]) -> usize
// structs being passed here are probably rare; some VMs might be usable even w/o implementing support for them?
struct phydat {
v1: u16,
v2: u16,
v3: u16,
n_v: u8,
unit: u16,
exponent: u8,
}
saul_write(Handler<saul reg>, data: phydat)
saul_read(Handler<saul reg>) -> phydat
// you could also do it like this (but really I'd rather have a better phydat)
// saul_read(Handler<saul reg>) -> Handler<phydat>
// phydat_get_ndim(Handler<phydat>) -> u8
// CoAP: This is a higher-level API than we have in GCoAP / nanocoap, but
// still not as high-level as I'd prefer it to (more REST-engine-like)
//
// (And it's simplified :-( )
coap_server_resource_new(name_hint: Option<&[u8]>) -> Handler<coap server>
coap_server_resource_request_pending(Handler<coap server>) -> Option<cred_id>
coap_message_take_option_u16(Handler<coap any>, optnum: u16) -> Option<u16>
coap_message_take_body(Handler<coap any>, &mut [u8]) -> usize
coap_message_prepare_response(Handler<coap server>) -> bool
coap_message_set_option_u16(Handler<coap any>, optnum: u16, optval: u16)
coap_message_set_body(Handler<coap any>, &mut [u8])
coap_message_send_response(Handler<coap server>)
// But I'd rather have a callback interface in the other direction
// we've not discussed events in detail, this is based on much guesswork
timer_set_ms(time: u32) -> Handle<wakeup timer event>
timer_cancel(Handle<wakeup timer event>) -> bool
// usage: pause_vm(), and if you're waiting for any of multiple events,
// just check whether handler_is_valid() has gone to false on any of the
// events?
// (or we could really block, I don't care so much)
pause_vm()
// or do we do callbacks? (that'd only work well if the VM's main function
// returned, and the VM were called again whenever any handler thought it'd
// be a good time to call a CB?)
timer_set_cb(Handle<wakup timer event>, fn())
puts(buffer: &[u8])
get_board_name(buffer: &mut [u8]) -> usize
vfs_root() -> Handle<vfs dir>
vfs_opendir_at(Handle<vfs dir>, name: &[u8]) -> Handle<vfs dir>
vfs_openfile_at(Handle<vfs dir>, name: &[u8]) -> Handle<vfs file>
vfs_readdir(Handle<vfs dir>) -> Handle<vfs dirent>
vfs_next(Handle<vfs dirent>)
// access vfs dirent by dedicated calls, or just return the full value in next?
vfs_opendir_ent(Handle<vfs dirent>) -> Handle<vfs dir>
vfs_openfile_ent(Handle<vfs dirent>) -> Handle<vfs file>
// How do we want to do error handling?
vfs_file_readslice(Handle<vfs file>, offset: size_t, buf: &mut [u8]) -> usize
vfs_file_replace(Handle<vfs file>, buf: &[u8])
vfs_close(Handle<vfs any>)
header code (either hand-written, or generated just to catch if we change our syscalls):
uint16_t syscall_random_u16(vm_index_t vm);
/* ... */
handwritten code:
enum vm_handler_type {
/* No (valid) file or other structure open at this index */
HT_INVALID = 0,
/* The saul_reg union field is valid and not null. */
HT_SAUL_REG,
}
typedef struct {
enum vm_handler_type type;
union {
saul_reg_t *saul_reg;
/* ... */
} value;
} vm_handler_t;
typedef uint8_t handler_index_t;
/* A handler index that is never used, and thus always of type HT_INVALID */
const handler_index_t HANDLER_NONE = 0xff;
uint16_t syscall_random_u16(vm_index_t vm) {
(void) vm; /* Every VM is eligible for RNG access */
return random_uint32();
}
uint32_t syscall_random_u32(vm_index_t vm) {
(void) vm; /* Every VM is eligible for RNG access */
return random_uint32();
}
void syscall_fill_random_buffer(vm_index_t vm, vm_ptr_t buffer__data, size_t buffer__len) {
/* No privilege checking: Every VM is eligible for RNG access */
uint8_t *buffer = vm_memmap_slice(vm, buffer__data, buffer__len);
if buffer == NULL {
/* It's important to halt the machine here: a VM with no
* support for memmapping should not just continue silently
* without random data. */
vm_panic(vm);
return;
}
random_bytes(buffer, buffer__len);
}
handler_index_t saul_iter_init(vm_index_t vm) {
if (!vm_access_allow_saul(vm)) {
return HANDLER_NONE;
}
handler_index_t result;
handler_t *handler = vm_allocate_handler(vm, &mut result);
if (result == NULL) {
return result;
}
handler->value.saul_reg = saul_reg_find_nth(0);
if (handler->value.saul_reg != NULL) {
handler->type = HT_SAUL;
}
return result;
}
void saul_iter_next(vm_index_t vm, handler_index_t handler) {
/* No need to do privilege checking: The VM has already obtained
* a SAUL handler */
handler_t *handler = vm_get_handler(vm, handler, HT_SAUL);
if (handler == NULL) {
return;
}
handler->value.saul_reg = handler->value.saul_reg->next;
if (handler->value.saul_reg == NULL) {
handler->type = HT_INVALID;
}
}
size_t saul_read_name(vm_index_t vm, handler_index_t handler, vm_ptr_t buffer__data, size_t buffer__len) {
handler_t *handler = vm_get_handler(vm, handler, HT_SAUL);
if (handler == NULL) {
return 0;
}
uint8_t *buffer = vm_memmap_slice(vm, buffer__data, buffer__len);
if (buffer == NULL) {
return 0;
}
if (handler->value.saul_reg->name == NULL) {
return 0;
}
/* FIXME: I don't know how C functions work precisely when it comes
* to trailing NUL bytes */
return strncpy(buffer, handler->value.saul_reg->name, buffer__len);
}
embedVM side (generated, in a bowdlerized of the Python-ish language I wrote a compiler for Back Then)
def random_u16() -> int:
syscall1(0)
return stack_pop()
# The compiler is really doing a best-effort job
# mapping str to MemviewU8, and completely ignores
# whether anything is read or write
def fill_random_buffer(buffer: MemviewU8):
stack_push(buffer.ptr)
stack_push(buffer.len)
syscall1(2)
return stack_pop()
def saul_iter_init() -> Handler:
syscall1(5)
return stack_pop()
Example program:
def main():
puts("Hello, this is RIOT on ")
board_name: MemviewU8[16] = MemviewU8()
board_name_len = get_board_name(board_name)
puts(board_name[:board_name_len])
puts("!\n")
C side (generated; bowdlerized)
case 0:
embedvm_push(syscall_random_u16(VM_NUMBER));
break;
case 2:
/* pop always returns an int16_t, but that happens to also be the pointer format we use */
vm_ptr_t buffer__ptr = embedvm_pop();
size_t buffer__len = embedvm_pop();
syscall_fill_random_buffer(VM_NUMBER, buffer__ptr, buffer__len);
break;
case 5:
embedvm_push(syscall_saul_iter_init(VM_NUMBER));
break;
C side (hand-written; bowdlerized):
/* This is assuming we only have one fixed VM */
#define OUR_VM_INDEX 1
void run_machine() {
uint16_t our_memory[512] = { PROGRAM };
/* set up VM struct */
vm_setup_memmaps(OUR_VM_INDEX, (uint8_t*)our_memory, sizeof(our_memory)));
vm_setup_permissions(OUR_VM_INDEX, VM_PERMISSIONS_ADMIN);
while (embedvm_running) {
embedvm_step();
switch embedvm_syscall_pending() {
#define VM_NUMBER OUR_VM_INDEX
#include "generated.c"
#undef VM_NUMBER
default:
continue;
}
}
}
Yes – the compiled code would be rolling both sides into one, and may need to do some tricks around handlers (they may not be movable).
Running Rust code would almost-but-not-quite be sandboxed when doing that – but we’d need to check that:
This is getting really verbose really quickly in many cases, and different languages have completely different idioms.
Can we get away with any error condition just irrecoverably halting the VM?