Skip to content

Commit

Permalink
Improve pointer documentation of the applet API (#717)
Browse files Browse the repository at this point in the history
This also fixes some soundness issues in the Rust prelude.
  • Loading branch information
ia0 authored Jan 7, 2025
1 parent e9ebc00 commit f965099
Show file tree
Hide file tree
Showing 87 changed files with 381 additions and 192 deletions.
4 changes: 3 additions & 1 deletion crates/api-desc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Minor

- Improve safety documentation
- Add `Api::wasm_markdown()` for top-level documentation
- Use Rust edition 2024
- Use C-string literals to implement dispatch for native applets
- Implement `bytemuck::Pod` for generated `Params`
Expand Down Expand Up @@ -137,4 +139,4 @@

## 0.1.0

<!-- Increment to skip CHANGELOG.md test: 1 -->
<!-- Increment to skip CHANGELOG.md test: 2 -->
11 changes: 10 additions & 1 deletion crates/api-desc/crates/update/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ fn main() -> Result<()> {
"#,
)?;
Api::default().wasm(&mut output, flags.lang.into())?;
let api = Api::default();
for line in api.wasm_markdown().lines() {
write!(&mut output, "//")?;
if !line.is_empty() {
write!(&mut output, " ")?;
}
writeln!(&mut output, "{}", line.strip_prefix("##").unwrap_or(line))?;
}
writeln!(&mut output)?;
api.wasm(&mut output, flags.lang.into())?;
Ok(())
}
78 changes: 78 additions & 0 deletions crates/api-desc/src/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
### Applet functions

An applet must expose 3 functions to the platform:

- `init: () -> ()` called exactly once before anything else. It cannot call any
platform function except `dp` (aka `debug::println()`).
- `main: () -> ()` called exactly once after `init` returns. It can call all
platform functions.
- `alloc: (size: u32, align: u32) -> (ptr: u32)` may be called during any
platform function after `init` returned. It cannot call any platform function.
A return value of zero indicates an error and will trap the applet. The applet
may assume that `size` is a non-zero multiple of `align` and `align` is either
1, 2, or 4.

### Platform functions

The platform exposes many functions. Each function comes with its own
documentation, which may omit the following general properties.

#### Parameters and result

At WebAssembly level, all functions use `u32` (more precisely `i32`) as
parameters and result (WebAssembly permits multiple results, but platform
functions never return more than one value). However, platform functions will be
documented with more specific types to better convey the intent.

Languages that compile to WebAssembly may use similar specific types when
binding platform functions. Those types should be compatible with `u32` in
WebAssembly.

#### Applet closures

Some functions (usually called `register`) may register a closure. There is
usually an associated function (usually called `unregister`) that unregisters
the registered closure. The register function takes `fn { data: *const void, ...
}` (usually called `handler_func`) and a `*const void` (usually called
`handler_data`) as arguments. The unregister function (if it exists) doesn't
have any particular parameters.

After the closure is registered (the register function is called) and as long as
it stays registered (the unregister function is not called), the platform may
call this closure any time `sw` (aka `scheduling::wait_for_callback()`) is
called. Note that if `main` returns and there are registered closures, the
applet behaves as if `sw` is called indefinitely.

An applet closure may call any platform function unless explicitly documented by
the register function.

#### Reading and writing memory

Some functions take pointers as argument (usually `*const u8` or `*mut u8`).
They either document the expected size or take an associated length argument.

The platform may read and write (only for `*mut T` pointers) the designated
region of the WebAssembly memory during the function call. Otherwise (and by
default), platform functions don't read or write the WebAssembly memory.

#### Allocating memory

Finally, some functions take nested pointers as argument (usually `*mut *mut
u8`). They either return a `usize` or take a `*mut usize` as argument.

Those functions may call `alloc` (at most once per such nested pointer argument)
with a dynamic size but fixed alignment (usually 1). If they do, they will store
the non-null result in the `*mut *mut u8` argument. Regardless of whether they
allocate or not, they store the size in the `*mut usize` argument (if it exists)
or return it (otherwise).

Note that `alloc` will not be called with a size of zero. So if the size is zero
after the function returns, this signifies that `alloc` was not called (and the
`*mut *mut u8` not written) and the output is empty (unless otherwise documented
like in the case of an absence of output, in which case the size may not be
written).

The caller is responsible for managing the allocation after the function returns
and the size is non-zero. To avoid memory leaks, the applet should initialize
the `*mut usize` argument (if it exists) to zero and consider having ownership
of the `*mut *mut u8` pointee if the size is non-zero after the call.
12 changes: 8 additions & 4 deletions crates/api-desc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ impl Api {
}
}

pub fn wasm_markdown(&self) -> &'static str {
include_str!("api.md")
}

pub fn wasm_rust(&self) -> TokenStream {
let items: Vec<_> = self.0.iter().map(|x| x.wasm_rust()).collect();
quote!(#(#items)*)
Expand Down Expand Up @@ -730,7 +734,7 @@ mod tests {
#[test]
fn link_names_are_unique() {
let mut seen = HashSet::new();
let Api(mut todo) = Api::default();
let mut todo = Api::default().0;
while let Some(item) = todo.pop() {
match item {
Item::Enum(_) => (),
Expand All @@ -746,7 +750,7 @@ mod tests {
/// This invariant is assumed by unsafe code.
#[test]
fn enum_values_are_valid() {
let Api(mut todo) = Api::default();
let mut todo = Api::default().0;
while let Some(item) = todo.pop() {
match item {
Item::Enum(Enum { variants, .. }) => {
Expand All @@ -773,7 +777,7 @@ mod tests {
let name = &field.name;
assert!(field.type_.is_param(), "Param {name} of {link:?} is not U32");
}
let Api(mut todo) = Api::default();
let mut todo = Api::default().0;
while let Some(item) = todo.pop() {
match item {
Item::Enum(_) => (),
Expand All @@ -786,7 +790,7 @@ mod tests {

#[test]
fn results_are_valid() {
let Api(mut todo) = Api::default();
let mut todo = Api::default().0;
while let Some(item) = todo.pop() {
match item {
Item::Enum(_) => (),
Expand Down
18 changes: 4 additions & 14 deletions crates/api-desc/src/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,31 +31,21 @@ pub(crate) fn new() -> Item {
update::new(),
#[cfg(feature = "api-platform")]
item! {
/// Returns the serial of the platform.
/// Reads the serial of the platform.
///
/// Returns the length of the serial in bytes. The serial is not allocated if the
/// length is zero (and the pointer is not written).
/// This is an [allocating function](crate#allocating-memory) returning the length.
fn serial "ps" {
/// Where to write the serial.
///
/// If the returned length is positive, the (inner) pointer will be allocated by the
/// callee and must be freed by the caller. It is thus owned by the caller when the
/// function returns.
ptr: *mut *mut u8,
} -> usize
},
#[cfg(feature = "api-platform")]
item! {
/// Returns the version of the platform.
/// Reads the version of the platform.
///
/// Returns the length of the version in bytes. The version is not allocated if the
/// length is zero (and the pointer is not written).
/// This is an [allocating function](crate#allocating-memory) returning the length.
fn version "pv" {
/// Where to write the version.
///
/// If the returned length is positive, the (inner) pointer will be allocated by the
/// callee and must be freed by the caller. It is thus owned by the caller when the
/// function returns.
ptr: *mut *mut u8,
} -> usize
},
Expand Down
7 changes: 3 additions & 4 deletions crates/api-desc/src/platform/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ pub(crate) fn new() -> Item {
item! {
/// Reads the last request, if any.
///
/// Returns whether a request was allocated.
/// Returns whether a request was read.
///
/// This is an [allocating function](crate#allocating-memory).
fn read "ppr" {
/// Where to write the request, if any.
///
/// The (inner) pointer will be allocated by the callee and must be freed by the
/// caller. It is thus owned by the caller when the function returns.
ptr: *mut *mut u8,

/// Where to write the length of the request, if any.
Expand Down
7 changes: 5 additions & 2 deletions crates/api-desc/src/platform/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ pub(crate) fn new() -> Item {
fn is_supported "pus" {} -> bool
},
item! {
/// Returns the metadata of the platform.
/// Reads the metadata of the platform.
///
/// This typically contains the version and side (A or B) of the running platform.
/// The metadata typically contains the version and side (A or B) of the running
/// platform.
///
/// This is an [allocating function](crate#allocating-memory).
fn metadata "pum" {
/// Where to write the allocated metadata.
ptr: *mut *mut u8,
Expand Down
14 changes: 5 additions & 9 deletions crates/api-desc/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ pub(crate) fn new() -> Item {
/// Finds an entry in the store, if any.
///
/// Returns whether an entry was found.
///
/// This is an [allocating function](crate#allocating-memory).
fn find "sf" {
/// Key of the entry to find.
key: usize,

/// Where to write the value of the entry, if found.
///
/// The (inner) pointer will be allocated by the callee and must be freed by the
/// caller. It is thus owned by the caller when the function returns.
ptr: *mut *mut u8,

/// Where to write the length of the value, if found.
Expand All @@ -74,13 +73,10 @@ pub(crate) fn new() -> Item {
item! {
/// Returns the unordered keys of the entries in the store.
///
/// Returns the number of keys, and thus the length of the array. The array is not
/// allocated if the length is zero (and the pointer is not written).
/// This is an [allocating function](crate#allocating-memory) returning the number of
/// keys (thus half the number of allocated bytes).
fn keys "sk" {
/// Where to write the keys as an array of u16, if at least one.
///
/// The (inner) pointer will be allocated by the callee and must be freed by the
/// caller. It is thus owned by the caller when the function returns.
/// Where to write the keys as an array of u16.
ptr: *mut *mut u8,
} -> usize
},
Expand Down
5 changes: 2 additions & 3 deletions crates/api-desc/src/store/fragment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,13 @@ pub(crate) fn new() -> Item {
/// The entry may be fragmented withen the provided range.
///
/// Returns whether an entry was found.
///
/// This is an [allocating function](crate#allocating-memory).
fn find "sff" {
/// Range of keys to concatenate as an entry.
keys: u32,

/// Where to write the value of the entry, if found.
///
/// The (inner) pointer will be allocated by the callee and must be freed by the
/// caller. It is thus owned by the caller when the function returns.
ptr: *mut *mut u8,

/// Where to write the length of the value, if found.
Expand Down
7 changes: 5 additions & 2 deletions crates/api-desc/src/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ pub(crate) fn new() -> Item {
},
item! {
/// Allocates a timer (initially stopped) and returns its identifier.
///
/// This is a [register function](crate#applet-closures). The associated unregister
/// function is [`free()`].
fn allocate "ta" {
/// Function called when the timer triggers.
handler_func: fn { data: *const void },
Expand Down Expand Up @@ -63,14 +66,14 @@ pub(crate) fn new() -> Item {
/// Note that if the timer triggers while being stopped, the handler may still be
/// called.
fn stop "tc" {
/// The identifier of the timer to start.
/// The identifier of the timer to stop.
id: usize,
} -> ()
},
item! {
/// Deallocates a stopped timer given its identifier.
fn free "td" {
/// The identifier of the timer to start.
/// The identifier of the timer to free.
id: usize,
} -> ()
},
Expand Down
3 changes: 2 additions & 1 deletion crates/api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Minor

- Improve safety documentation
- Use Rust edition 2024
- Provide `bytemuck::Pod` for `ArrayU32`
- Implement `bytemuck::Pod` for `U32<T>`
Expand Down Expand Up @@ -106,4 +107,4 @@

## 0.1.0

<!-- Increment to skip CHANGELOG.md test: 1 -->
<!-- Increment to skip CHANGELOG.md test: 2 -->
1 change: 1 addition & 0 deletions crates/api/src/api.md
2 changes: 2 additions & 0 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#![cfg_attr(feature = "host", doc = "Platform-side of the applet API.")]
#![cfg_attr(feature = "wasm", doc = include_str!("wasm.md"))]
#![cfg_attr(feature = "wasm", doc = "\n## WebAssembly-level documentation\n")]
#![cfg_attr(feature = "wasm", doc = include_str!("api.md"))]
#![no_std]
#![cfg_attr(all(feature = "wasm", feature = "native"), feature(linkage))]
#![cfg_attr(feature = "host", feature(never_type))]
Expand Down
7 changes: 6 additions & 1 deletion crates/prelude/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Changelog

## 0.7.1-git
## 0.8.0-git

### Major

- Make `syscall()` unsafe to call

### Minor

- Support the new behavior of the scheduler (not allocating if the size is zero)
- Add `rng::bytes{,_array}()` as safer alternatives to `rng::fill_bytes()`
- Use Rust edition 2024

Expand Down
2 changes: 1 addition & 1 deletion crates/prelude/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/prelude/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "wasefire"
version = "0.7.1-git"
version = "0.8.0-git"
authors = ["Julien Cretin <[email protected]>"]
license = "Apache-2.0"
publish = true
Expand Down
9 changes: 5 additions & 4 deletions crates/prelude/src/allocator/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,11 @@ extern "C" fn init() {

#[unsafe(no_mangle)]
extern "C" fn alloc(size: u32, align: u32) -> u32 {
let layout = match Layout::from_size_align(size as usize, align as usize) {
Ok(x) => x,
Err(_) => return 0,
};
let Ok(layout) = Layout::from_size_align(size as usize, align as usize) else { return 0 };
if size == 0 {
return 0; // this is not checked by Layout::from_size_align()
}
// SAFETY: Layout has non-zero size.
unsafe { ALLOCATOR.alloc(layout) as u32 }
}

Expand Down
Loading

0 comments on commit f965099

Please sign in to comment.