Skip to content

Commit

Permalink
sys: Add stdio_coap
Browse files Browse the repository at this point in the history
  • Loading branch information
chrysn committed Feb 20, 2023
1 parent 523a39a commit faa7637
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
# Cargo.toml, copy this file over, or just use the released versions.

[patch.crates-io]
riot-sys = { git = "https://github.com/RIOT-OS/rust-riot-sys" }
riot-wrappers = { git = "https://github.com/RIOT-OS/rust-riot-wrappers" }
riot-sys = { git = "https://github.com/RIOT-OS/rust-riot-sys", branch = "marker-for-config-auto-init-enable-debug" }
riot-wrappers = { git = "https://github.com/RIOT-OS/rust-riot-wrappers", branch = "auto-init" }
3 changes: 3 additions & 0 deletions makefiles/cargo-targets.inc.mk
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ endif
ifneq (,$(filter shell_democommands,$(USEMODULE)))
CARGO_OPTIONS += --features rust_riotmodules/riot-module-shell-democommands
endif
ifneq (,$(filter stdio_coap,$(USEMODULE)))
CARGO_OPTIONS += --features rust_riotmodules/riot-module-stdio-coap
endif

# This is duplicating the compile-commands rule because unlike in the use case
# when a $(RIOTBASE)/compile_commands.json is built, we *want* this to be
Expand Down
18 changes: 18 additions & 0 deletions makefiles/stdio.inc.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
STDIO_MODULES = \
slipdev_stdio \
stdio_cdc_acm \
stdio_coap \
stdio_ethos \
stdio_native \
stdio_nimble \
Expand All @@ -25,6 +26,23 @@ ifneq (,$(filter stdio_cdc_acm,$(USEMODULE)))
USEMODULE += stdio_available
endif

ifneq (,$(filter stdio_coap,$(USEMODULE)))
# FIXME: Does this obsolete the stdio_coap Makefile.{dep,include}?
USEMODULE += rust_riotmodules
PSEUDOMODULES += stdio_coap

# FIXME: Whose responsibility is it to include these?

# either
USEMODULE += netdev_default
#USEMODULE += usbus_cdc_ecm

USEMODULE += gnrc_sock_udp
USEMODULE += gcoap
USEMODULE += gnrc_ipv6_default
USEMODULE += auto_init_gnrc_netif
endif

ifneq (,$(filter stdio_tinyusb_cdc_acm,$(USEMODULE)))
USEPKG += tinyusb
endif
Expand Down
1 change: 1 addition & 0 deletions sys/rust_riotmodules/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ publish = false

riot-module-lsm303agr = { path = "../../drivers/lsm303agr", optional = true }
riot-module-shell-democommands = { path = "../../sys/shell/democommands", optional = true }
riot-module-stdio-coap = { path = "../../sys/stdio_coap", optional = true }
3 changes: 3 additions & 0 deletions sys/rust_riotmodules/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ pub use riot_module_lsm303agr as lsm303agr;

#[cfg(feature = "riot-module-shell-democommands")]
pub use riot_module_shell_democommands as democommands;

#[cfg(feature = "riot-module-stdio-coap")]
pub use riot_module_stdio_coap as stdio_coap;
4 changes: 2 additions & 2 deletions sys/rust_riotmodules_standalone/Cargo.lock

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

24 changes: 24 additions & 0 deletions sys/stdio_coap/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "riot-module-stdio-coap"
version = "0.1.0"
edition = "2021"

authors = ["Christian Amsüss <chrysn@fsfe.org>"]
license = "LGPL-2.1-only"

# Shipped with RIOT-OS; this has no external API that would make
# sense to consume in any context than from within RIOT
publish = false

[dependencies]
coap-scroll-ring-server = "0.1"
riot-wrappers = { version = "^0.8", features = [ "with_coap_handler" ] } # esp. the git version: fixes mutex bug
riot-sys = "0.7"
scroll-ring = "0.1.1"
static_cell = "1"
# for stdin
coap-handler = "0.1"
coap-message = "0.2"
coap-numbers = "0.2"
ringbuf = { version = "0.3.2", default-features = false }
coap-handler-implementations = { git = "https://gitlab.com/chrysn/coap-handler-implementations", features = [ "leaky_names" ] }
197 changes: 197 additions & 0 deletions sys/stdio_coap/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
//! An implementation of stdio based on CoAP
//!
//! The stdout side is trivial in that it builds on stdout being a [scroll_ring::Buffer] and using
//! the implementation provided in [coap_scroll_ring_server] to expose it through CoAP.
//!
//! The stdin side is a manual implementation of a non-overwriting MPMC queue (which rejects
//! excessive input with a 503 error)
#![no_std]

use coap_handler_implementations::{HandlerBuilder, ReportingHandlerBuilder};
use ringbuf::{ring_buffer::RbBase, Rb};
use riot_wrappers::mutex::Mutex;

mod stdin;
use stdin::{Stdin, StdinHandler};

const STDOUT_BUFSIZE: usize = 1024;
const STDIN_BUFSIZE: usize = 128;

static mut STDOUT: Option<&'static scroll_ring::Buffer<STDOUT_BUFSIZE>> = None;

// FIXME: The whole construction of Stdin, STDIN and STDIN_WAITER could very well be described by a
// blocking SPMC thread-safe ring buffer (which we don't have in RIOT right now)

/// This is never held for long, and only awaited blockingly while also holding STDIN_WAITER.
///
/// It is only None during initialization.
static STDIN: Mutex<Option<&'static mut Stdin<STDIN_BUFSIZE>>> = Mutex::new(None);
/// A lock of this is stored in STDIN while any reader is waiting.
static STDIN_WAITER: Mutex<()> = Mutex::new(());

/// The CoAP handler for the stdout resource
///
/// If you do not use the auto-initialized CoAP tree, this handler needs to be placed discoverably
/// somewhere in the CoAP resource tree to make stdout accessible.
pub fn stdout_handler() -> coap_scroll_ring_server::BufferHandler<'static, STDOUT_BUFSIZE> {
coap_scroll_ring_server::BufferHandler::new(unsafe { STDOUT }.expect("stdio was configured"))
}
///
/// The CoAP handler for the stdin resource
///
/// If you do not use the auto-initialized CoAP tree, this handler needs to be placed discoverably
/// somewhere in the CoAP resource tree to make stdin accessible.
pub fn stdin_handler() -> coap_handler_implementations::wkc::ConstantSingleRecordReport<
'static,
StdinHandler<STDIN_BUFSIZE>,
> {
coap_handler_implementations::wkc::ConstantSingleRecordReport::new(
StdinHandler(&STDIN),
&[coap_handler::Attribute::Interface(
"tag:riot-os.org,2021:ser-in",
)],
)
}

riot_wrappers::auto_init!(inittest, 65535);
fn inittest() {
riot_wrappers::println!("Coming up");
}

// FIXME: Declaring this creates a hard dependency on the auto_init module -- but we don't have an
// easy way to spot whether it's active right now. This practically makes the stdio_coap module
// depend on auto_init.

riot_wrappers::auto_init!(auto_init_stdio_coap_late, 42);

fn auto_init_stdio_coap_late() {
let handler = coap_handler_implementations::new_dispatcher()
.at(&["so"], stdout_handler())
.at(&["si"], stdin_handler())
.with_wkc();

// If we had feature(type_alias_impl_trait), we could use this, and drop the leaky_names
// feature:
// type HT = impl riot_wrappers::gcoap::Handler + 'static;
// (By the way, I didn't come up with that type name ... I boxed things and had type_name
// printed :-) )
type HT = riot_wrappers::coap_handler::GcoapHandler<
coap_handler_implementations::WellKnownCore<
coap_handler_implementations::ForkingHandler<
'static,
coap_handler_implementations::wkc::ConstantSingleRecordReport<
'static,
StdinHandler<STDIN_BUFSIZE>,
>,
coap_handler_implementations::ForkingHandler<
'static,
coap_scroll_ring_server::BufferHandler<'static, STDOUT_BUFSIZE>,
coap_handler_implementations::wkc::NotReporting<
coap_handler_implementations::NeverFound,
>,
>,
>,
>,
>;
static HANDLER: static_cell::StaticCell<HT> = static_cell::StaticCell::new();
static LISTENER: static_cell::StaticCell<riot_wrappers::gcoap::SingleHandlerListener<HT>> =
static_cell::StaticCell::new();

let handler = HANDLER.init(riot_wrappers::coap_handler::GcoapHandler(handler));
let listener = LISTENER.init(riot_wrappers::gcoap::SingleHandlerListener::new_catch_all(
handler,
));

riot_wrappers::gcoap::register(listener);
}

#[no_mangle]
pub extern "C" fn stdio_init() {
static SO: static_cell::StaticCell<scroll_ring::Buffer<STDOUT_BUFSIZE>> =
static_cell::StaticCell::new();
// Calling this twice would panic
let so = SO.init_with(Default::default);

// unsafe: There's only one place in the code that writes there, and this is guarded in the
// previous line by a run-once condition.
//
// (And it may be safely read all the time, because there is no parallelism yet when this is
// initialized)
unsafe { STDOUT = Some(so) };

static SI: static_cell::StaticCell<Stdin<STDIN_BUFSIZE>> = static_cell::StaticCell::new();
let si = SI.init_with(|| Stdin {
lock_for_waiter: None,
buffer: Default::default(),
});
*STDIN.try_lock().unwrap() = Some(si);
}

#[no_mangle]
extern "C" fn stdio_write(
buffer: *const riot_sys::libc::c_void,
len: riot_sys::size_t,
) -> riot_sys::size_t {
// unsafe: See its one writer.
let stdout = unsafe { STDOUT };

// unsafe: data is initialized per API
let data = unsafe { core::slice::from_raw_parts(buffer as *const u8, len as _) };

if let Some(stdout) = stdout {
stdout.write(data);
data.len() as _
} else {
// stdout not initialized
0
}
}

#[no_mangle]
extern "C" fn stdio_read(
buffer: *mut riot_sys::libc::c_void,
max_len: riot_sys::size_t,
) -> riot_sys::ssize_t {
// API adjustments for type readability
let mut buffer = buffer as *mut u8;
let mut max_len = max_len as usize;

if max_len == 0 {
// The later loop requires that it can count down max_len after each popped item (which
// requires popping first and checking after decrement).
return 0;
}

loop {
let waiter_lock = STDIN_WAITER.lock();

// There's no guarantee that every reader from stdio is in a single thread, although that
// appears to be a common assumption.
let mut stdin = STDIN.lock();
let mut stdin = stdin.as_mut().expect("STDIN was initialized");

if stdin.buffer.is_empty() {
// Move away waiter_lock on STDIN_WAITER, so when we're next entering the loop we're
// blocked until there has been progress on stdin
stdin.lock_for_waiter = Some(waiter_lock);
drop(stdin);
continue;
}

let mut assigned = 0;
// This might be doable more efficiently, but then again, stdio_read is usually called with
// a 1-long buffer anyway.
for b in stdin.buffer.pop_iter() {
// unsafe: Per C API
unsafe { *buffer = b };
// unsafe: Per C API
buffer = unsafe { buffer.offset(1) };
max_len -= 1;
assigned += 1;
if max_len == 0 {
break;
}
}
return assigned;
}
}
72 changes: 72 additions & 0 deletions sys/stdio_coap/src/stdin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use riot_wrappers::mutex::{Mutex, MutexGuard};
use ringbuf::Rb;

pub(crate) struct Stdin<const N: usize> {
pub(crate) lock_for_waiter: Option<MutexGuard<'static, ()>>,
pub(crate) buffer: ringbuf::LocalRb<u8, [core::mem::MaybeUninit<u8>; N]>,
}

pub struct StdinHandler<const N: usize>(pub(crate) &'static Mutex<Option<&'static mut Stdin<N>>>);

impl<const N: usize> coap_handler::Handler for StdinHandler<N> {
type RequestData = u8;
fn extract_request_data(&mut self, req: &impl coap_message::ReadableMessage) -> u8 {
use coap_handler_implementations::option_processing::OptionsExt;

let opts = req.options().ignore_elective_others();

if opts.is_err() {
// FIXME: We could also use its Render implementation if that were public
return coap_numbers::code::BAD_OPTION;
}

if req.code().into() != coap_numbers::code::POST {
return coap_numbers::code::METHOD_NOT_ALLOWED;
}

let Some(mut stdin) = self.0.try_lock() else {
// This only happens when someone is just in the process of locking stdin -- we'll need
// to return in ordrr to let them place their mutex there. FIXME: Max-Age:0
return coap_numbers::code::SERVICE_UNAVAILABLE;
};
let Some(stdin) = stdin.as_mut() else {
// Stdin not initialized
//
// Returning as before here not only because it's a sensible error, but also because it
// should allow the compiler to collapse the branches.
return coap_numbers::code::SERVICE_UNAVAILABLE;
};

let maybe_the_waiter = stdin.lock_for_waiter.take();

if stdin.buffer.free_len() < req.payload().len() {
if req.payload().len() > N {
// Buffer full. FIXME: Max-Age? Too-Many-Requests? Request-Entity-Too-Large?
return coap_numbers::code::SERVICE_UNAVAILABLE;
} else {
// Not a chance
return coap_numbers::code::REQUEST_ENTITY_TOO_LARGE;
}
}
stdin.buffer.push_slice(req.payload());

drop(maybe_the_waiter);

coap_numbers::code::CHANGED
}
fn estimate_length(&mut self, _req: &u8) -> usize {
1
}
fn build_response(&mut self, res: &mut impl coap_message::MutableWritableMessage, req: u8) {
res.set_code(req.try_into().ok().unwrap());
if req == coap_numbers::code::REQUEST_ENTITY_TOO_LARGE {
if let (Ok(opt), Ok(size)) = (
coap_numbers::option::SIZE1.try_into(),
u16::try_from(N),
) {
res.add_option_uint(opt, size);
}
}
res.set_payload(b"");
}
}

0 comments on commit faa7637

Please sign in to comment.