diff --git a/dt-rust.yaml b/dt-rust.yaml index c848277..6ab0c9f 100644 --- a/dt-rust.yaml +++ b/dt-rust.yaml @@ -45,6 +45,7 @@ - "nordic,nrf52-flash-controller" - "nordic,nrf51-flash-controller" - "raspberrypi,pico-flash-controller" + - "st,stm32-flash-controller" - "zephyr,sim-flash" level: 0 actions: diff --git a/main.c b/main.c index fb6c10f..44da93d 100644 --- a/main.c +++ b/main.c @@ -52,4 +52,12 @@ void rust_panic_wrap(void) k_panic(); } +/* `errno` is usually defined as a macro rather than a variable, which makes it difficult to + * access in Rust. So create a simple getter function in C. + */ +int get_errno(void) +{ + return errno; +} + #endif diff --git a/samples/net/echo_server/CMakeLists.txt b/samples/net/echo_server/CMakeLists.txt new file mode 100644 index 0000000..f3e0573 --- /dev/null +++ b/samples/net/echo_server/CMakeLists.txt @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 OR MIT + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(echo_server) + +rust_cargo_application() diff --git a/samples/net/echo_server/Cargo.toml b/samples/net/echo_server/Cargo.toml new file mode 100644 index 0000000..a5e1a79 --- /dev/null +++ b/samples/net/echo_server/Cargo.toml @@ -0,0 +1,21 @@ +# Copyright (c) 2025 Witekio +# SPDX-License-Identifier: Apache-2.0 + +[package] +# This must be rustapp for now. +name = "rustapp" +version = "0.1.0" +edition = "2021" +description = "Echo's back any data received from a UDP network socket to the sender" +license = "Apache-2.0 OR MIT" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +zephyr = "0.1.0" +log = "0.4.22" +static_cell = "2.1.0" + +[build-dependencies] +zephyr-build = "0.1.0" diff --git a/samples/net/echo_server/boards/qemu_cortex_m3.conf b/samples/net/echo_server/boards/qemu_cortex_m3.conf new file mode 100644 index 0000000..d1accb6 --- /dev/null +++ b/samples/net/echo_server/boards/qemu_cortex_m3.conf @@ -0,0 +1,3 @@ +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_NET_QEMU_ETHERNET=y +CONFIG_NET_L2_ETHERNET=y diff --git a/samples/net/echo_server/build.rs b/samples/net/echo_server/build.rs new file mode 100644 index 0000000..f3849d1 --- /dev/null +++ b/samples/net/echo_server/build.rs @@ -0,0 +1,9 @@ +fn main() { + // This call will make make config entries available in the code for every device tree node, to + // allow conditional compilation based on whether it is present in the device tree. + // For example, it will be possible to have: + // ```rust + // #[cfg(dt = "aliases::led0")] + // ``` + zephyr_build::dt_cfgs(); +} diff --git a/samples/net/echo_server/prj.conf b/samples/net/echo_server/prj.conf new file mode 100644 index 0000000..ee008ab --- /dev/null +++ b/samples/net/echo_server/prj.conf @@ -0,0 +1,23 @@ +CONFIG_RUST=y +CONFIG_RUST_ALLOC=y # For threads +CONFIG_MAIN_STACK_SIZE=2048 + +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_UDP=y +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y + +# Net config settings for sample +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV6=y +CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_CONFIG_PEER_IPV6_ADDR="2001:db8::2" +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" +CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2" + +CONFIG_SHELL=y +CONFIG_NET_SHELL=y diff --git a/samples/net/echo_server/sample.yaml b/samples/net/echo_server/sample.yaml new file mode 100644 index 0000000..52d76dd --- /dev/null +++ b/samples/net/echo_server/sample.yaml @@ -0,0 +1,15 @@ +# See doc/develop/test/twister.rst for what is here. +sample: + name: Echo Server Sample +common: + filter: CONFIG_RUST_SUPPORTED + platform_allow: + - qemu_cortex_m3 + - qemu_riscv32 + - qemu_riscv64 +tests: + sample.rust.net.echo_server: + tags: + - net + depends_on: netif + harness: net diff --git a/samples/net/echo_server/src/lib.rs b/samples/net/echo_server/src/lib.rs new file mode 100644 index 0000000..cf4c240 --- /dev/null +++ b/samples/net/echo_server/src/lib.rs @@ -0,0 +1,108 @@ +// Copyright (c) 2025 Witekio +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![allow(unexpected_cfgs)] + +use log::{error, info, warn}; +use static_cell::ConstStaticCell; +use zephyr::error::Result; +use zephyr::kobj_define; +use zephyr::net::{TcpListener, TcpStream, UdpSocket}; + +#[no_mangle] +extern "C" fn rust_main() { + unsafe { + zephyr::set_logger().unwrap(); + } + + // Don't allocate the large RX buffers on the stack + static UDP_RX_BUF: ConstStaticCell<[u8; 2048]> = ConstStaticCell::new([0; 2048]); + static TCP_RX_BUF: ConstStaticCell<[u8; 2048]> = ConstStaticCell::new([0; 2048]); + let udp_rx_buf = UDP_RX_BUF.take(); + let tcp_rx_buf = TCP_RX_BUF.take(); + + // Create the sockets + let sockaddr = "0.0.0.0:4242".parse().unwrap(); + let udp_sock = UdpSocket::bind(&sockaddr).expect("Failed to bind UdpSocket"); + let mut tcp_listener = TcpListener::bind(&sockaddr).expect("Failed to bind TcpListener"); + + info!("Echo server bound to address {:?}", sockaddr); + + // Spawn the UDP echo server in a thread + let udp_thread = UDP_THREAD + .init_once(UDP_STACK.init_once(()).unwrap()) + .unwrap(); + + udp_thread.spawn(move || { + if let Err(e) = run_udp_echo(udp_sock, udp_rx_buf) { + error!("UDP echo thread failed with error {:?}", e); + } + }); + + // Run the TCP echo server in the main thread + loop { + match tcp_listener.accept() { + Ok((sock, peer)) => { + info!("Accepted connection from peer address {:?}", peer); + let _ = run_tcp_echo(sock, tcp_rx_buf); + } + Err(e) => { + error!("Failed to accept TCP connection with error {:?}", e); + break; + } + } + } + + info!("TCP echo server exited"); +} + +fn run_udp_echo(sock: UdpSocket, buf: &mut [u8]) -> Result<()> { + loop { + let (n, peer) = sock.recv_from(buf)?; + + // Note that being able to set the MSG_TRUNC sockopt is not implemented yet so it should + // not be possible to get n > rx_buf.len(), but it's probably still worth including the + // check for when this sockopt is implemented. + let n_trunc = n.min(buf.len()); + if n != n_trunc { + warn!("Data truncated, got {} / {} bytes", n_trunc, n); + } + + info!( + "Echoing {} bytes to peer address {:?} via UDP", + n_trunc, peer + ); + let _ = sock.send_to(&buf[0..n_trunc], &peer)?; + } +} + +fn run_tcp_echo(mut sock: TcpStream, buf: &mut [u8]) -> Result<()> { + loop { + let n = sock.recv(buf)?; + + if 0 == n { + info!("TCP client disconected"); + return Ok(()); + } + + info!("Echoing {} bytes via TCP", n); + let mut remaining = &buf[0..n]; + while !remaining.is_empty() { + match sock.send(remaining)? { + 0 => { + info!("TCP client disconnected"); + return Ok(()); + } + n => remaining = &remaining[n..], + }; + } + } +} + +const STACK_SIZE: usize = zephyr::kconfig::CONFIG_MAIN_STACK_SIZE as usize; + +kobj_define! { + static UDP_THREAD: StaticThread; + static UDP_STACK: ThreadStack; +} diff --git a/tests/net/tcp/CMakeLists.txt b/tests/net/tcp/CMakeLists.txt new file mode 100644 index 0000000..efc4d9a --- /dev/null +++ b/tests/net/tcp/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(tcp_rust) + +rust_cargo_application() diff --git a/tests/net/tcp/Cargo.toml b/tests/net/tcp/Cargo.toml new file mode 100644 index 0000000..3cadb16 --- /dev/null +++ b/tests/net/tcp/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 Witekio +# SPDX-License-Identifier: Apache-2.0 + +[package] +# This must be rustapp for now. +name = "rustapp" +version = "0.1.0" +edition = "2021" +description = "Tests of TCP sockets" +license = "Apache-2.0 or MIT" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +zephyr = "0.1.0" diff --git a/tests/net/tcp/prj.conf b/tests/net/tcp/prj.conf new file mode 100644 index 0000000..aed7763 --- /dev/null +++ b/tests/net/tcp/prj.conf @@ -0,0 +1,22 @@ +# Copyright (c) 2025 Witekio +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_RUST=y +CONFIG_RUST_ALLOC=y # For threads +CONFIG_MAIN_STACK_SIZE=2048 + +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_PKT_RX_COUNT=16 +CONFIG_NET_PKT_TX_COUNT=16 + +# Networking drivers +CONFIG_NET_DRIVERS=y +CONFIG_NET_TEST=y +CONFIG_NET_LOOPBACK=y +CONFIG_TEST_RANDOM_GENERATOR=y + diff --git a/tests/net/tcp/src/lib.rs b/tests/net/tcp/src/lib.rs new file mode 100644 index 0000000..f9c38e8 --- /dev/null +++ b/tests/net/tcp/src/lib.rs @@ -0,0 +1,61 @@ +// Copyright (c) 2025 Witekio +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use zephyr::net::{TcpListener, TcpStream}; +use zephyr::printkln; + +#[no_mangle] +extern "C" fn rust_main() { + let tx_data: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; + let mut server_recv_buf: [u8; 64] = [0; 64]; + let mut client_recv_buf: [u8; 64] = [0; 64]; + + // The test creates client and server TcpStreams, and echo's some data between them to verify + // that the TCP connection is functioning + printkln!("Starting tests"); + + let server_sockaddr = "0.0.0.0:4242" + .parse() + .expect("Failed to parse server sockaddr"); + + let connect_sockaddr = "127.0.0.1:4242" + .parse() + .expect("Failed to parse connect sockaddr"); + + let mut listener = TcpListener::bind(&server_sockaddr).expect("Failed to create TcpListener"); + + let mut client_stream = + TcpStream::connect(&connect_sockaddr).expect("Failed to create client TcpStream"); + + let sent = client_stream + .send(&tx_data) + .expect("Client failed to send data"); + + assert_eq!(sent, tx_data.len()); + + let (mut server_stream, _) = listener.accept().expect("Failed to accept connection"); + + let recvd = server_stream + .recv(&mut server_recv_buf) + .expect("Server failed to receive data"); + + assert_eq!(recvd, tx_data.len()); + assert_eq!(server_recv_buf[0..recvd], tx_data); + + let sent = server_stream + .send(&server_recv_buf[0..recvd]) + .expect("Failed to send data"); + + assert_eq!(sent, recvd); + + let recvd = client_stream + .recv(&mut client_recv_buf) + .expect("Client failed to receive data"); + + assert_eq!(recvd, sent); + assert_eq!(client_recv_buf[0..recvd], tx_data); + + printkln!("All tests passed"); +} diff --git a/tests/net/tcp/testcase.yaml b/tests/net/tcp/testcase.yaml new file mode 100644 index 0000000..2469121 --- /dev/null +++ b/tests/net/tcp/testcase.yaml @@ -0,0 +1,15 @@ +common: + filter: CONFIG_RUST_SUPPORTED + platform_allow: + - qemu_cortex_m0 + - qemu_cortex_m3 + - qemu_riscv32 + - qemu_riscv64 + - nrf52840dk/nrf52840 +tests: + test.rust.tcp: + harness: console + harness_config: + type: one_line + regex: + - "All tests passed" diff --git a/tests/net/udp/CMakeLists.txt b/tests/net/udp/CMakeLists.txt new file mode 100644 index 0000000..8178c1a --- /dev/null +++ b/tests/net/udp/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(udp_rust) + +rust_cargo_application() diff --git a/tests/net/udp/Cargo.toml b/tests/net/udp/Cargo.toml new file mode 100644 index 0000000..c0a5c98 --- /dev/null +++ b/tests/net/udp/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 Witekio +# SPDX-License-Identifier: Apache-2.0 + +[package] +# This must be rustapp for now. +name = "rustapp" +version = "0.1.0" +edition = "2021" +description = "Tests of UDP sockets" +license = "Apache-2.0 or MIT" + +[lib] +crate-type = ["staticlib"] + +[dependencies] +zephyr = "0.1.0" diff --git a/tests/net/udp/prj.conf b/tests/net/udp/prj.conf new file mode 100644 index 0000000..3cf28e8 --- /dev/null +++ b/tests/net/udp/prj.conf @@ -0,0 +1,19 @@ +# Copyright (c) 2025 Witekio +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_RUST=y +CONFIG_MAIN_STACK_SIZE=2048 + +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_UDP=y +CONFIG_NET_SOCKETS=y + +# Networking drivers +CONFIG_NET_DRIVERS=y +CONFIG_NET_TEST=y +CONFIG_NET_LOOPBACK=y +CONFIG_TEST_RANDOM_GENERATOR=y + diff --git a/tests/net/udp/src/lib.rs b/tests/net/udp/src/lib.rs new file mode 100644 index 0000000..2d7ed20 --- /dev/null +++ b/tests/net/udp/src/lib.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2025 Witekio +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] + +use zephyr::net::UdpSocket; +use zephyr::printkln; + +#[no_mangle] +extern "C" fn rust_main() { + printkln!("Starting tests"); + + let rx_sa = "127.0.0.1:4242" + .parse() + .expect("Failed to parse RX SocketAddr"); + let tx_sa = "127.0.0.1:12345" + .parse() + .expect("Failed to parse TX SocketAddr"); + + let rx_sock = UdpSocket::bind(&rx_sa).expect("Failed to create RX UDP socket"); + let tx_sock = UdpSocket::bind(&tx_sa).expect("Failed to create TX UDP socket"); + + let tx_data: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; + let sent = tx_sock + .send_to(&tx_data, &rx_sa) + .expect("Failed to send data"); + assert_eq!(sent, tx_data.len()); + + let mut rx_data: [u8; 8] = [0; 8]; + let (recvd, peer) = rx_sock + .recv_from(&mut rx_data) + .expect("Failed to receive data"); + assert_eq!(recvd, tx_data.len()); + assert_eq!(tx_data, rx_data); + assert_eq!(peer, tx_sa); + + printkln!("All tests passed"); +} diff --git a/tests/net/udp/testcase.yaml b/tests/net/udp/testcase.yaml new file mode 100644 index 0000000..21799c9 --- /dev/null +++ b/tests/net/udp/testcase.yaml @@ -0,0 +1,15 @@ +common: + filter: CONFIG_RUST_SUPPORTED + platform_allow: + - qemu_cortex_m0 + - qemu_cortex_m3 + - qemu_riscv32 + - qemu_riscv64 + - nrf52840dk/nrf52840 +tests: + test.rust.udp: + harness: console + harness_config: + type: one_line + regex: + - "All tests passed" diff --git a/zephyr-sys/build.rs b/zephyr-sys/build.rs index 05c6e94..92035c5 100644 --- a/zephyr-sys/build.rs +++ b/zephyr-sys/build.rs @@ -88,7 +88,16 @@ fn main() -> Result<()> { .allowlist_function("sys_.*") .allowlist_function("z_log.*") .allowlist_function("bt_.*") + .allowlist_function("zsock_.*") .allowlist_function("SEGGER.*") + .allowlist_item("AF_.*") + .allowlist_type("net_ip_protocol.*") + .allowlist_type("net_sock_type") + .allowlist_type("sockaddr.*") + .allowlist_type("sa_family_t") + .allowlist_type("socklen_t") + .allowlist_type("in_addr") + .allowlist_type("in6_addr") .allowlist_item("E.*") .allowlist_item("K_.*") .allowlist_item("ZR_.*") diff --git a/zephyr-sys/wrapper.h b/zephyr-sys/wrapper.h index 98bb957..921eca4 100644 --- a/zephyr-sys/wrapper.h +++ b/zephyr-sys/wrapper.h @@ -42,6 +42,7 @@ extern int errno; #include #include #include +#include /* * bindgen will only output #defined constants that resolve to simple numbers. These are some diff --git a/zephyr/Cargo.toml b/zephyr/Cargo.toml index bb70549..d112140 100644 --- a/zephyr/Cargo.toml +++ b/zephyr/Cargo.toml @@ -22,6 +22,9 @@ cfg-if = "1.0" log = "0.4.22" arrayvec = { version = "0.7.6", default-features = false } +# The num crate is used primarily for the Zero trait bound +num = { version = "0.4", default-features = false } + [dependencies.fugit] version = "0.3.7" diff --git a/zephyr/src/error.rs b/zephyr/src/error.rs index 35d1602..880e8cc 100644 --- a/zephyr/src/error.rs +++ b/zephyr/src/error.rs @@ -11,8 +11,14 @@ //! this an enum itself, however, it would probably be better to auto-generate this enum instead of //! trying to maintain the list manually. +use core::cmp::PartialOrd; use core::ffi::c_int; use core::fmt; +use num::Signed; + +extern "C" { + fn get_errno() -> c_int; +} // This is a little messy because the constants end up as u32 from bindgen, although the values are // negative. @@ -51,6 +57,20 @@ pub fn to_result(code: c_int) -> Result { } } +/// Map a return result and errno from Zephyr into a Result. +/// +/// Some Zephyr functions (particularly those intended for compatibility with POSIX function +/// signatures) return -1 on error, with the specific error code stored in the `errno` variable. +#[inline(always)] +pub fn to_result_errno(code: T) -> Result { + if code.is_negative() { + let e = unsafe { get_errno() }; + Err(Error(e as u32)) + } else { + Ok(code) + } +} + /// Map a return result, with a void result. #[inline(always)] pub fn to_result_void(code: c_int) -> Result<()> { diff --git a/zephyr/src/lib.rs b/zephyr/src/lib.rs index 7ccb767..1f4c2c6 100644 --- a/zephyr/src/lib.rs +++ b/zephyr/src/lib.rs @@ -52,6 +52,7 @@ //! also a [`kio::sync::Mutex`] type that works with async. //! - [`logging`]: A logging backend for Rust on Zephyr. This will log to either `printk` or //! through Zephyr's logging framework. +//! - [`net`]: Networking support based on top of Zephyr's sockets API. //! //! [`Instant`]: time::Instant //! [`Duration`]: time::Duration @@ -83,6 +84,7 @@ pub mod error; #[cfg(CONFIG_RUST_ALLOC)] pub mod kio; pub mod logging; +pub mod net; pub mod object; #[cfg(CONFIG_RUST_ALLOC)] pub mod simpletls; diff --git a/zephyr/src/net/ipaddr.rs b/zephyr/src/net/ipaddr.rs new file mode 100644 index 0000000..82fa784 --- /dev/null +++ b/zephyr/src/net/ipaddr.rs @@ -0,0 +1,191 @@ +//! Conversion between Rust and C representations of IP and Socket addresses. +//! +//! The intention is that the Rust networking API presents an interface using the types defined in +//! [`core::net`](https://doc.rust-lang.org/stable/core/net/index.html). Conversion to and from the +//! C types should only be required inside this crate. + +use crate::error::{Error, Result}; +use crate::raw::{in6_addr, in_addr, sockaddr, sockaddr_in, sockaddr_in6, socklen_t}; +use core::mem::MaybeUninit; +use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; + +/// Convert an IPv4 address from Rust to C representation +pub(crate) fn ipv4_addr_to_c(addr: &Ipv4Addr) -> in_addr { + let octets = addr.octets(); + + // SAFETY: It's not possible to easily construct this type due to the BindgenUnion field, and in + // any case constructing it would always require unsafe code. + // + // The C type and the octets field of the Rust type both have a well-defined layout: 4 bytes in + // big-endian order. So it is safe to transmute between the two types. + unsafe { core::mem::transmute(octets) } +} + +/// Convert an IPv4 address from C to Rust representation +pub(crate) fn ipv4_addr_from_c(addr: &in_addr) -> Ipv4Addr { + // SAFETY: The s4_addr union field has no restrictions on which values are valid, so is always + // safe to access. + let s4_addr = unsafe { addr.__bindgen_anon_1.s4_addr.as_ref() }; + + Ipv4Addr::new(s4_addr[0], s4_addr[1], s4_addr[2], s4_addr[3]) +} + +/// Convert an IPv6 address from Rust to C representation +pub(crate) fn ipv6_addr_to_c(addr: &Ipv6Addr) -> in6_addr { + let octets = addr.octets(); + + // SAFETY: It's not possible to easily construct this type due to the BindgenUnion field, and in + // any case constructing it would always require unsafe code. + // + // The C type and the octets field of the Rust type both have a well-defined layout: 16 bytes in + // big-endian order. So it is safe to transmute between the two types. + unsafe { core::mem::transmute(octets) } +} + +/// Convert an IPv6 address from C to Rust representation +pub(crate) fn ipv6_addr_from_c(addr: &in6_addr) -> Ipv6Addr { + // SAFETY: The s6_addr16 union field has no restrictions on which values are valid, so is always + // safe to access. + let s6_addr16 = unsafe { addr.__bindgen_anon_1.s6_addr16.as_ref() }; + + Ipv6Addr::new( + s6_addr16[0], + s6_addr16[1], + s6_addr16[2], + s6_addr16[3], + s6_addr16[4], + s6_addr16[5], + s6_addr16[6], + s6_addr16[7], + ) +} + +/// Convert an IPv4 socket address from Rust to C representation +pub(crate) fn sockaddr_v4_to_c(sa: &SocketAddrV4) -> sockaddr_in { + sockaddr_in { + sin_family: crate::raw::AF_INET as u16, + sin_port: sa.port().to_be(), + sin_addr: ipv4_addr_to_c(sa.ip()), + } +} + +/// Try to convert an IPv4 socket address from C to Rust representation +/// +/// This will return an error if the `sin_family` field of `sa` is not `AF_INET`. +pub(crate) fn try_sockaddr_v4_from_c(sa: &sockaddr_in) -> Result { + if sa.sin_family != crate::raw::AF_INET as u16 { + return Err(Error(crate::raw::EINVAL)); + } + + Ok(SocketAddrV4::new( + ipv4_addr_from_c(&sa.sin_addr), + u16::from_be(sa.sin_port), + )) +} + +/// Try to convert an IPv6 socket address from Rust to C representation +/// +/// This will return an error in either of the following cases: +/// - The `flowinfo` field is non-zero +/// - The `scope_id` field cannot be represented as a `u8` +/// +/// Zephyr's `struct sockaddr_in6` differs slightly from the traditional BSD sockets API in that +/// it does not contain a `flowinfo` field, and the `scope_id` is stored as a `uint8_t`. +pub(crate) fn try_sockaddr_v6_to_c(sa: &SocketAddrV6) -> Result { + if sa.flowinfo() != 0 || sa.scope_id() > u8::MAX as u32 { + return Err(Error(crate::raw::EINVAL)); + } + + Ok(sockaddr_in6 { + sin6_family: crate::raw::AF_INET6 as u16, + sin6_port: sa.port().to_be(), + sin6_addr: ipv6_addr_to_c(sa.ip()), + sin6_scope_id: sa.scope_id() as u8, + }) +} + +/// Try to convert an IPv6 socker address from C to Rust representation +/// +/// This will return an error if the `sin6_family` field of `sa` is not `AF_INET6`. +pub(crate) fn try_sockaddr_v6_from_c(sa: &sockaddr_in6) -> Result { + if sa.sin6_family != crate::raw::AF_INET6 as u16 { + return Err(Error(crate::raw::EINVAL)); + } + + Ok(SocketAddrV6::new( + ipv6_addr_from_c(&sa.sin6_addr), + u16::from_be(sa.sin6_port), + 0, + sa.sin6_scope_id as u32, + )) +} + +/// Try to convert a socket address from Rust to C representation +/// +/// This will return an error if the socket address is a `SocketAddrV6`, and this address contains +/// fields which cannot be represented in the C struct (see [try_sockaddr_v6_from_c]). +pub(crate) fn try_sockaddr_to_c(sa: &SocketAddr) -> Result<(sockaddr, socklen_t)> { + let mut sa_out = MaybeUninit::::zeroed(); + let dst_ptr = sa_out.as_mut_ptr() as *mut u8; + + let socklen = match sa { + SocketAddr::V4(sa4) => { + let src = sockaddr_v4_to_c(sa4); + let src_ptr = (&src as *const sockaddr_in) as *const u8; + let len = core::mem::size_of::(); + + // SAFETY: The `sockaddr` struct is sized to be able to contain either a `sockaddr_in` + // or a `sockaddr_in6`, so it is safe to copy either type into it. + unsafe { + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len); + } + + len + } + SocketAddr::V6(sa6) => { + let src = try_sockaddr_v6_to_c(sa6)?; + let src_ptr = (&src as *const sockaddr_in6) as *const u8; + let len = core::mem::size_of::(); + + // SAFETY: The `sockaddr` struct is sized to be able to contain either a `sockaddr_in` + // or a `sockaddr_in6`, so it is safe to copy either type into it. + unsafe { + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, len); + } + + len + } + }; + + // SAFETY: Both match arms initialise `sa_out` with a valid `sockaddr` + let sa_out = unsafe { sa_out.assume_init() }; + + Ok((sa_out, socklen)) +} + +/// Try to convert a socket address from C to Rust representation +/// +/// This will return an error if the `sa_family` field is not either `AF_INET` or `AF_INET6`. +pub(crate) fn try_sockaddr_from_c(sa: &sockaddr, socklen: socklen_t) -> Result { + match sa.sa_family as u32 { + crate::raw::AF_INET => { + if socklen != core::mem::size_of::() { + return Err(Error(crate::raw::EINVAL)); + } + + let sa4_ref: &sockaddr_in = unsafe { core::mem::transmute(sa) }; + let res = try_sockaddr_v4_from_c(sa4_ref)?; + Ok(SocketAddr::V4(res)) + } + crate::raw::AF_INET6 => { + if socklen != core::mem::size_of::() { + return Err(Error(crate::raw::EINVAL)); + } + + let sa6_ref: &sockaddr_in6 = unsafe { core::mem::transmute(sa) }; + let res = try_sockaddr_v6_from_c(sa6_ref)?; + Ok(SocketAddr::V6(res)) + } + _ => Err(Error(crate::raw::EINVAL)), + } +} diff --git a/zephyr/src/net/mod.rs b/zephyr/src/net/mod.rs new file mode 100644 index 0000000..b2dfcb3 --- /dev/null +++ b/zephyr/src/net/mod.rs @@ -0,0 +1,117 @@ +//! Networking support. +//! +//! This module provides support for networking over UDP. +//! +//! Currently only a blocking networking API is provided, which builds on top of Zephyr's sockets +//! API. + +mod ipaddr; +mod socket; + +use crate::error::Result; +use crate::net::socket::{Domain, Protocol, SockType, Socket}; +use core::net::SocketAddr; + +/// UDP socket. +/// +/// A UDP socket can be created by binding to a local socket address. Once bound, data can be sent +/// and received using [`send_to`] and [`recv_from`]. +/// +/// [`send_to`]: UdpSocket::send_to +/// [`recv_from`]: UdpSocket::recv_from +pub struct UdpSocket { + sock: Socket, +} + +impl UdpSocket { + /// Create a UDP socket bound to the provided socket address. + pub fn bind(addr: &SocketAddr) -> Result { + let domain = Domain::from_socket_addr(addr); + + let mut sock = Socket::new(domain, SockType::Dgram, Protocol::IpProtoUdp)?; + sock.bind(addr)?; + + Ok(Self { sock }) + } + + /// Send data to the specified socket address, returning the number of bytes that were actually + /// sent. + pub fn send_to(&self, buf: &[u8], addr: &SocketAddr) -> Result { + self.sock.send_to(buf, addr) + } + + /// Receive from the socket, returning the data length and peer address it was received from. + pub fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { + self.sock.recv_from(buf) + } +} + +/// TCP Listener +/// +/// A TCP listener can be created by binding to a local socket address. It represents a server +/// socket which is ready to accept incoming connections. +/// +/// Connections can be accepted by calling [`accept`] on the TcpListener. +/// +/// [`accept`]: TcpListener::accept +pub struct TcpListener { + sock: Socket, +} + +impl TcpListener { + /// Create a TCP listener bound to and listening on the provided socket address. + pub fn bind(addr: &SocketAddr) -> Result { + let domain = Domain::from_socket_addr(addr); + + let mut sock = Socket::new(domain, SockType::Stream, Protocol::IpProtoTcp)?; + sock.bind(addr)?; + sock.listen()?; + + Ok(Self { sock }) + } + + /// Accept a single connection from the TCP listener. + pub fn accept(&mut self) -> Result<(TcpStream, SocketAddr)> { + let (sock, peer) = self.sock.accept()?; + Ok((TcpStream { sock }, peer)) + } +} + +/// TCP Stream +/// +/// This represents a connected TCP socket, which may be either a client or server socket. Data can +/// be sent and received over the stream using the [`send`] and [`recv`] methods. +/// +/// There are two ways to get a `TcpStream`: +/// - Call [`accept`] on a [`TcpListener`] to get a connected stream as a server. +/// - Use [`connect`] to get a connected stream as a client +/// +/// [`accept`]: TcpListener::accept +/// [`connect`]: TcpStream::connect +/// [`send`]: TcpStream::send +/// [`recv`]: TcpStream::recv +pub struct TcpStream { + sock: Socket, +} + +impl TcpStream { + /// Create a TCP stream by connecting to the provided socket address. + pub fn connect(addr: &SocketAddr) -> Result { + let domain = Domain::from_socket_addr(addr); + + let mut sock = Socket::new(domain, SockType::Stream, Protocol::IpProtoTcp)?; + sock.connect(addr)?; + + Ok(Self { sock }) + } + + /// Send data over the TCP stream, returning the number of bytes that were actually sent. + pub fn send(&mut self, buf: &[u8]) -> Result { + self.sock.send(buf) + } + + /// Receive data from the TCP stream, returning the number of bytes received. + pub fn recv(&mut self, buf: &mut [u8]) -> Result { + self.sock.recv(buf) + } +} diff --git a/zephyr/src/net/socket.rs b/zephyr/src/net/socket.rs new file mode 100644 index 0000000..29e6088 --- /dev/null +++ b/zephyr/src/net/socket.rs @@ -0,0 +1,184 @@ +//! Low-level network socket support +//! +//! A minimal safe Rust wrapper around Zephyr's sockets API, intended for higher level Rust +//! abstractions to be implemented on top of. + +use crate::error::{to_result_errno, Result}; +use crate::net::ipaddr::{try_sockaddr_from_c, try_sockaddr_to_c}; +use crate::raw::{self, sockaddr, socklen_t}; +use core::ffi::{c_int, c_void}; +use core::mem::MaybeUninit; +use core::net::SocketAddr; + +#[derive(Debug, Copy, Clone)] +pub(crate) enum Domain { + AfInet = crate::raw::AF_INET as isize, + AfInet6 = crate::raw::AF_INET6 as isize, +} + +impl Domain { + /// Get the Domain associated with a SocketAddr + pub(crate) fn from_socket_addr(addr: &SocketAddr) -> Self { + match addr { + SocketAddr::V4(_) => Self::AfInet, + SocketAddr::V6(_) => Self::AfInet6, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum SockType { + Dgram = crate::raw::net_sock_type_SOCK_DGRAM as isize, + Stream = crate::raw::net_sock_type_SOCK_STREAM as isize, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum Protocol { + IpProtoUdp = crate::raw::net_ip_protocol_IPPROTO_UDP as isize, + IpProtoTcp = crate::raw::net_ip_protocol_IPPROTO_TCP as isize, +} + +// We could make the backlog configurable, but for now just pick a sensible default value +const LISTEN_BACKLOG: c_int = 10; + +/// Socket type implementing minimal safe wrappers around Zephyr's C socket API +pub(crate) struct Socket { + fd: c_int, +} + +impl Socket { + /// Create a socket + /// + /// This is a minimal wrapper around zsock_socket + pub(crate) fn new(domain: Domain, sock_type: SockType, protocol: Protocol) -> Result { + let res = + unsafe { raw::zsock_socket(domain as c_int, sock_type as c_int, protocol as c_int) }; + + let fd = to_result_errno(res)?; + + Ok(Socket { fd }) + } + + /// Bind a socket + /// + /// This is a minimal wrapper around zsock_bind + pub(crate) fn bind(&mut self, addr: &SocketAddr) -> Result<()> { + let (sockaddr, socklen) = try_sockaddr_to_c(addr)?; + + let res = unsafe { raw::zsock_bind(self.fd, &sockaddr as *const sockaddr, socklen) }; + + let _ = to_result_errno(res)?; + Ok(()) + } + + /// Connect a socket to a peer socket address + /// + /// This is a minimal wrapper around zsock_connect + pub(crate) fn connect(&mut self, peer: &SocketAddr) -> Result<()> { + let (sa, socklen) = try_sockaddr_to_c(peer)?; + + let res = unsafe { raw::zsock_connect(self.fd, &sa as *const sockaddr, socklen) }; + let _ = to_result_errno(res)?; + Ok(()) + } + + /// Listen for incoming connections on a socket + /// + /// This is a minimal wrapper around zsock_listen + pub(crate) fn listen(&mut self) -> Result<()> { + let res = unsafe { raw::zsock_listen(self.fd, LISTEN_BACKLOG) }; + let _ = to_result_errno(res)?; + Ok(()) + } + + /// Accept a connection on a listening socket + /// + /// This is a minimal wrapper around zsock_accept + pub(crate) fn accept(&mut self) -> Result<(Socket, SocketAddr)> { + let mut sa = MaybeUninit::::uninit(); + let mut socklen: socklen_t = core::mem::size_of::(); + + let res = + unsafe { raw::zsock_accept(self.fd, sa.as_mut_ptr(), &mut socklen as *mut socklen_t) }; + + let new_fd = to_result_errno(res)?; + let new_sock = Socket { fd: new_fd }; + + // SAFETY: `zsock_accept` returned a success code, so it has populated the sockaddr. + let sa = unsafe { sa.assume_init() }; + + let peer_sa = try_sockaddr_from_c(&sa, socklen)?; + Ok((new_sock, peer_sa)) + } + + /// Receive from the socket, returning the data length and peer address it was received from + /// + /// This is a minimal wrapper around zsock_recvfrom + pub(crate) fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { + let mut sa = MaybeUninit::::uninit(); + let mut socklen: socklen_t = core::mem::size_of::(); + + let res = unsafe { + raw::zsock_recvfrom( + self.fd, + buf.as_mut_ptr() as *mut c_void, + buf.len(), + 0, + sa.as_mut_ptr(), + &mut socklen as *mut socklen_t, + ) + }; + + let recvd = to_result_errno(res)?; + + // SAFETY: `zsock_recvfrom` returned a success code, so it has populated the sockaddr. + let sa = unsafe { sa.assume_init() }; + + let peer_sa = try_sockaddr_from_c(&sa, socklen)?; + Ok((recvd as usize, peer_sa)) + } + + /// Send data to the specified socket address. + /// + /// This is a minimal wrapper around zsock_sendto + pub fn send_to(&self, buf: &[u8], peer: &SocketAddr) -> Result { + let (sa, socklen) = try_sockaddr_to_c(peer)?; + + let res = unsafe { + raw::zsock_sendto( + self.fd, + buf.as_ptr() as *const c_void, + buf.len(), + 0, + &sa as *const sockaddr, + socklen, + ) + }; + + let sent = to_result_errno(res)?; + Ok(sent as usize) + } + + /// Send data on a socket + pub fn send(&mut self, buf: &[u8]) -> Result { + let res = unsafe { raw::zsock_send(self.fd, buf.as_ptr() as *const c_void, buf.len(), 0) }; + let sent = to_result_errno(res)?; + Ok(sent as usize) + } + + /// Receive data from a socket + pub fn recv(&mut self, buf: &mut [u8]) -> Result { + let res = + unsafe { raw::zsock_recv(self.fd, buf.as_mut_ptr() as *mut c_void, buf.len(), 0) }; + let recvd = to_result_errno(res)?; + Ok(recvd as usize) + } +} + +impl Drop for Socket { + fn drop(&mut self) { + unsafe { + let _ = raw::zsock_close(self.fd); + } + } +}