From ec0f3f073958d7544da427fe33382d79b1e827d2 Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Fri, 7 Nov 2025 06:41:34 +1100 Subject: [PATCH] Create no_std examples --- Cargo.toml | 3 +- README.md | 4 +- jaarg-nostd/.cargo/config.toml | 12 ++ jaarg-nostd/Cargo.toml | 10 ++ jaarg-nostd/README.md | 4 + jaarg-nostd/examples/basic_nostd.rs | 68 ++++++++ jaarg-nostd/examples/btreemap_nostd.rs | 46 ++++++ jaarg-nostd/src/harness.rs | 210 +++++++++++++++++++++++++ jaarg-nostd/src/lib.rs | 6 + jaarg-nostd/src/simplepathbuf.rs | 81 ++++++++++ jaarg/src/std.rs | 6 +- 11 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 jaarg-nostd/.cargo/config.toml create mode 100644 jaarg-nostd/Cargo.toml create mode 100644 jaarg-nostd/README.md create mode 100644 jaarg-nostd/examples/basic_nostd.rs create mode 100644 jaarg-nostd/examples/btreemap_nostd.rs create mode 100644 jaarg-nostd/src/harness.rs create mode 100644 jaarg-nostd/src/lib.rs create mode 100644 jaarg-nostd/src/simplepathbuf.rs diff --git a/Cargo.toml b/Cargo.toml index f2b864b..fad27b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] -members = ["jaarg"] +default-members = ["jaarg"] +members = ["jaarg-nostd"] resolver = "3" [workspace.package] diff --git a/README.md b/README.md index 27d83ac..04c10af 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,9 @@ main: * Moved `Opts::parse_map` into newly introduced `alloc` crate, making it accessible for `no_std` users. * API updates, enough public constructs to roll a custom help writer. * Generalised internal error & usage into `StandardErrorUsageWriter` for reuse outside the easy API & in `no_std`. - * Fixed uncontrollable newlines in user display in easy API. + * Fixed forced newline in user display in easy API. * More tests for validating internal behaviour & enabled CI on GitHub. + * New `no_std` examples. v0.1.1: * Fixed incorrect error message format for coerced parsing errors. @@ -75,7 +76,6 @@ v0.1.0: ### Roadmap ### Near future: - * Actual `no_std` tests & examples. * More control over parsing behaviour (getopt style, no special casing shorts for Windows style flags, etc.) * More practical examples. diff --git a/jaarg-nostd/.cargo/config.toml b/jaarg-nostd/.cargo/config.toml new file mode 100644 index 0000000..2ceb8ef --- /dev/null +++ b/jaarg-nostd/.cargo/config.toml @@ -0,0 +1,12 @@ +[target.'cfg(target_os = "macos")'] +rustflags = ["-Cpanic=abort", "-C", "link-args=-lSystem"] +[target.'cfg(target_family = "windows")'] +rustflags = ["-Cpanic=abort", "-C", "target-feature=+crt-static"] +[target.'cfg(target_os = "linux")'] +rustflags = ["-Cpanic=abort", "-C", "link-args=-lc"] + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" diff --git a/jaarg-nostd/Cargo.toml b/jaarg-nostd/Cargo.toml new file mode 100644 index 0000000..376f751 --- /dev/null +++ b/jaarg-nostd/Cargo.toml @@ -0,0 +1,10 @@ +[package] +publish = false +name = "jaarg-nostd" +description = "nostd examples for jaarg" +edition.workspace = true + +[dependencies.jaarg] +path = "../jaarg" +default-features = false +features = ["alloc"] diff --git a/jaarg-nostd/README.md b/jaarg-nostd/README.md new file mode 100644 index 0000000..ed9830c --- /dev/null +++ b/jaarg-nostd/README.md @@ -0,0 +1,4 @@ +# ⚠ ATTENTION ⚠ +Due to cargo limitations, these examples will fail to build & link unless +`cargo build` is ran from this directory. See `.cargo/config.toml` for +requisite build configuration. diff --git a/jaarg-nostd/examples/basic_nostd.rs b/jaarg-nostd/examples/basic_nostd.rs new file mode 100644 index 0000000..476b4ca --- /dev/null +++ b/jaarg-nostd/examples/basic_nostd.rs @@ -0,0 +1,68 @@ +/* basic_nostd - jaarg example program using parse in `no_std` + * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications + * SPDX-License-Identifier: MIT + */ + +#![no_std] +#![no_main] + +extern crate alloc; + +use jaarg::{ + ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts, + ParseControl, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter +}; +use jaarg_nostd::{print, println, harness::ExitCode, simplepathbuf::SimplePathBuf}; + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +extern "C" fn safe_main(args: &[&str]) -> ExitCode { + // Variables for arguments to fill + let mut file = SimplePathBuf::default(); + let mut out: Option = None; + let mut number = 0; + + // Set up arguments table + enum Arg { Help, Number, File, Out } + const OPTIONS: Opts = Opts::new(&[ + Opt::help_flag(Arg::Help, &["-h", "--help"]).help_text("Show this help and exit."), + Opt::value(Arg::Number, &["-n", "--number"], "value") + .help_text("Optionally specify a number (default: 0)"), + Opt::positional(Arg::File, "file").required() + .help_text("Input file."), + Opt::positional(Arg::Out, "out") + .help_text("Output destination (optional).") + ]).with_description("My simple utility."); + + // Parse command-line arguments from argv + match OPTIONS.parse( + SimplePathBuf::from(*args.first().unwrap()).basename(), + args.iter().skip(1), + |program_name, id, _opt, _name, arg| { + match id { + Arg::Help => { + let ctx = HelpWriterContext { options: &OPTIONS, program_name }; + print!("{}", StandardFullHelpWriter::<'_, Arg>::new(ctx)); + return Ok(ParseControl::Quit); + } + Arg::Number => { number = str::parse(arg)?; } + Arg::File => { file = arg.into(); } + Arg::Out => { out = Some(arg.into()); } + } + Ok(ParseControl::Continue) + }, |program_name, error| { + let ctx = ErrorUsageWriterContext { options: &OPTIONS, program_name, error }; + print!("{}", StandardErrorUsageWriter::<'_, Arg>::new(ctx)); + } + ) { + ParseResult::ContinueSuccess => (), + ParseResult::ExitSuccess => { return ExitCode::SUCCESS; } + ParseResult::ExitError => { return ExitCode::FAILURE; } + } + + // Print the result variables + println!("{file} -> {out} (number: {number})", + out = out.unwrap_or(file.with_extension("out"))); + + ExitCode::SUCCESS +} diff --git a/jaarg-nostd/examples/btreemap_nostd.rs b/jaarg-nostd/examples/btreemap_nostd.rs new file mode 100644 index 0000000..d20298b --- /dev/null +++ b/jaarg-nostd/examples/btreemap_nostd.rs @@ -0,0 +1,46 @@ +/* btreemap_nostd - jaarg example program using BTreeMap in `no_std` + * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications + * SPDX-License-Identifier: MIT + */ + +#![no_std] +#![no_main] + +extern crate alloc; + +use jaarg::{ + alloc::ParseMapResult, ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, + Opt, Opts, StandardErrorUsageWriter, StandardFullHelpWriter +}; +use jaarg_nostd::{eprint, print, println, harness::ExitCode, simplepathbuf::SimplePathBuf}; + +#[no_mangle] +#[allow(improper_ctypes_definitions)] +extern "C" fn safe_main(args: &[&str]) -> ExitCode { + const OPTIONS: Opts<&'static str> = Opts::new(&[ + Opt::help_flag("help", &["--help"]).help_text("Show this help"), + Opt::positional("positional", "positional").help_text("Positional argument"), + Opt::value("value", &["-v", "--value"], "string").help_text("Value option"), + Opt::flag("flag", &["-f", "--flag"]).help_text("Flag option"), + ]); + + let map = match OPTIONS.parse_map( + SimplePathBuf::from(*args.first().unwrap()).basename(), + args.iter().skip(1), + |name| { + let ctx = HelpWriterContext { options: &OPTIONS, program_name: name }; + print!("{}", StandardFullHelpWriter::new(ctx)); + }, + |program_name, err| { + let ctx = ErrorUsageWriterContext { options: &OPTIONS, program_name, error: err }; + eprint!("{}", StandardErrorUsageWriter::new(ctx)); + } + ) { + ParseMapResult::Map(map) => map, + ParseMapResult::ExitSuccess => { return ExitCode::SUCCESS; } + ParseMapResult::ExitFailure => { return ExitCode::FAILURE; } + }; + + println!("{:?}", map); + ExitCode::SUCCESS +} diff --git a/jaarg-nostd/src/harness.rs b/jaarg-nostd/src/harness.rs new file mode 100644 index 0000000..fd326dd --- /dev/null +++ b/jaarg-nostd/src/harness.rs @@ -0,0 +1,210 @@ +/* jaarg-nostd - Minimal harness to run examples in no_std on desktop + * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications + * SPDX-License-Identifier: MIT + */ + +//!! [Okay... ready for the pain?](https://media.tenor.com/cJRcMyUAiMcAAAAC/tenor.gif) + +use core::alloc::{GlobalAlloc, Layout}; +use core::fmt::Write; +#[allow(unused_imports)] +use core::panic::PanicInfo; + +/// Unix file descriptor +pub struct FileDescriptor(core::ffi::c_int); +#[allow(unused)] +impl FileDescriptor { + /// Standard input file descriptor + const STDIN: Self = Self(0); + /// Standard output file descriptor + const STDOUT: Self = Self(1); + /// Standard error file descriptor + const STDERR: Self = Self(2); +} + +pub struct StandardOutWriter; +impl Write for StandardOutWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + unsafe { + c::write(FileDescriptor::STDOUT.0, s.as_ptr() as *const core::ffi::c_void, s.len()); + } + Ok(()) + } +} + +pub fn print(args: core::fmt::Arguments) { + StandardOutWriter{}.write_fmt(args).unwrap(); +} + +pub struct StandardErrorWriter; +impl Write for StandardErrorWriter { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + unsafe { + c::write(FileDescriptor::STDERR.0, s.as_ptr() as *const core::ffi::c_void, s.len()); + } + Ok(()) + } +} + +pub fn eprint(args: core::fmt::Arguments) { + StandardErrorWriter{}.write_fmt(args).unwrap(); +} + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => {{ $crate::harness::print(format_args!($($arg)*)); }}; +} + +#[macro_export] +macro_rules! eprint { + ($($arg:tt)*) => {{ $crate::harness::eprint(format_args!($($arg)*)); }}; +} + +#[macro_export] +macro_rules! println { + () => {{ $crate::print!("\n"); }}; + ($($arg:tt)*) => {{ $crate::print!("{}\n", format_args!($($arg)*)); }}; +} + +#[macro_export] +macro_rules! eprintln { + () => {{ $crate::eprint!("\n"); }}; + ($($arg:tt)*) => {{ $crate::eprint!("{}\n", format_args!($($arg)*)); }}; +} + +/// Calls system abort +pub fn exit(status: i32) -> ! { + unsafe { c::exit(status as core::ffi::c_int) } +} + +/// Bare minimum malloc-based global allocator +#[derive(Default)] +pub struct MallocAlloc; +impl MallocAlloc { + // Fundamental alignment table: + // | Target | 32-bit | 64-bit | Note | + // |---------|--------|--------|-----------------------------| + // | macOS | 16 | 16 | Always 16-byte aligned | + // | GNU | 8 | 16 | | + // | Windows | 8 | 16 | nonstd aligned_alloc & free | + // | OpenBSD | 16 | 16 | FIXME: Unsourced | + // [Darwin source](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/MemoryAlloc.html) + // [GNU glibc source](https://sourceware.org/glibc/manual/2.42/html_node/Malloc-Examples.html) + // [Windows source](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/malloc?view=msvc-170) + + // if ptr == 32 && !(os == "macos" || os == "openbsd") + #[cfg(all(target_pointer_width = "32", not(any(target_os = "macos", target_os = "openbsd"))))] + const ALIGNMENT: Option = Some(8); + // if ptr == 64 || (ptr == 32 && (os == "macos" || os == "openbsd")) + #[cfg(any(target_pointer_width = "64", all(target_pointer_width = "32", any(target_os = "macos", target_os = "openbsd"))))] + const ALIGNMENT: Option = Some(16); + // if !(ptr == 32 || ptr == 64) + #[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))] + const ALIGNMENT: Option = None; + + /// If target alignment % requested_align == 0 then malloc is good enough. + #[inline(always)] + const fn layout_can_use_malloc(requested_layout: &Layout) -> bool { + let align = requested_layout.align(); + align != 0 && matches!(Self::ALIGNMENT, + Some(sys_align) if (sys_align & (align - 1)) == 0) + } +} +unsafe impl GlobalAlloc for MallocAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + if Self::layout_can_use_malloc(&layout) { + c::malloc(layout.size()).cast::() + } else { + #[cfg(target_family = "windows")] + return c::aligned_alloc(layout.size(), layout.align()).cast::(); + #[cfg(not(target_family = "windows"))] + return c::aligned_alloc(layout.align(), layout.size()).cast::(); + } + } + #[allow(unused_variables)] + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + #[cfg(target_family = "windows")] + if !Self::layout_can_use_malloc(&layout) { + c::aligned_free(ptr as *mut core::ffi::c_void); + return; + } + c::free(ptr as *mut core::ffi::c_void); + } +} + +#[global_allocator] +static GLOBAL_ALLOCATOR: MallocAlloc = MallocAlloc; + +/// Hurt me plenty (intellidumb will think this a lang duplicate pls ignore) +#[cfg(not(test))] +#[panic_handler] +unsafe fn panic(info: &PanicInfo) -> ! { + eprintln!("panic abort: {}", info.message()); + c::abort() +} + +/// Ultra-Violence +#[cfg(not(test))] +#[no_mangle] +pub extern "C" fn rust_eh_personality() {} + +/// Nightmare! +#[cfg(not(test))] +#[allow(non_snake_case)] +#[no_mangle] +extern "C" fn _Unwind_Resume() {} + +extern "C" { + #[allow(improper_ctypes)] + pub fn safe_main(args: &[&str]) -> ExitCode; +} + +/// Exit code to be passed to entry point wrapper. +#[allow(non_camel_case_types)] +#[repr(i32)] +pub enum ExitCode { + SUCCESS = 0, + FAILURE = 1, +} + +/// C main entry point, collects argc/argv and calls `safe_main`. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn main(argc: core::ffi::c_int, argv: *const *const core::ffi::c_char) -> core::ffi::c_int { + let mut args = alloc::vec::Vec::<&str>::with_capacity(argc as usize); + for i in 0..argc as usize { + args.push(core::ffi::CStr::from_ptr(*argv.wrapping_add(i)).to_str().unwrap()); + } + safe_main(&args) as core::ffi::c_int +} + +mod c { + use core::ffi::{c_int, c_void}; + + /// Until size_t is stabilised + #[allow(non_camel_case_types)] + type c_size_t = usize; + + #[allow(dead_code)] + extern "C" { + pub(crate) fn atexit(function: extern "C" fn()) -> c_int; + pub(crate) fn abort() -> !; + pub(crate) fn exit(status: c_int) -> !; + #[cfg(not(target_family = "windows"))] + pub(crate) fn write(fd: c_int, buf: *const c_void, bytes: c_size_t) -> c_int; + #[cfg(target_family = "windows")] + #[link_name = "_write"] + pub(crate) fn write(fd: c_int, buf: *const c_void, bytes: c_size_t) -> c_int; + pub(crate) fn malloc(size: c_size_t) -> *mut c_void; + pub(crate) fn calloc(count: c_size_t, size: c_size_t) -> *mut c_void; + #[cfg(not(target_family = "windows"))] + pub(crate) fn aligned_alloc(alignment: c_size_t, size: c_size_t) -> *mut c_void; + #[cfg(target_family = "windows")] + #[link_name = "_aligned_malloc"] + pub(crate) fn aligned_alloc(size: c_size_t, alignment: c_size_t) -> *mut c_void; + #[cfg(target_family = "windows")] + #[link_name = "_aligned_free"] + pub(crate) fn aligned_free(memblock: *mut c_void); + pub(crate) fn free(ptr: *mut c_void); + } +} diff --git a/jaarg-nostd/src/lib.rs b/jaarg-nostd/src/lib.rs new file mode 100644 index 0000000..6dda39c --- /dev/null +++ b/jaarg-nostd/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] + +extern crate alloc; + +pub mod harness; +pub mod simplepathbuf; diff --git a/jaarg-nostd/src/simplepathbuf.rs b/jaarg-nostd/src/simplepathbuf.rs new file mode 100644 index 0000000..1dea9fb --- /dev/null +++ b/jaarg-nostd/src/simplepathbuf.rs @@ -0,0 +1,81 @@ +/* jaarg-nostd - Minimal harness to run examples in no_std on desktop + * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications + * SPDX-License-Identifier: MIT + */ + +use alloc::format; +use alloc::string::String; +use core::fmt::{Display, Formatter}; + +/// Dirty and simple path buffer that's good enough for the `no_std` examples, not for production use. +#[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct SimplePathBuf(String); + +impl> From for SimplePathBuf where String: From { + fn from(value: S) -> Self { + Self(value.into()) + } +} + +impl Display for SimplePathBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl SimplePathBuf { + #[inline(always)] + fn path_predicate(c: char) -> bool { + #[cfg(target_family = "windows")] + if c == '\\' { return true; } + c == '/' + } + + pub fn with_extension(&self, ext: &str) -> Self { + let dir_sep = self.0.rfind(Self::path_predicate) + .map_or(0, |n| n + 1); + let without_ext: &str = self.0[dir_sep..].rfind('.') + .map_or(&self.0, |ext_sep_rel| { + if ext_sep_rel == 0 { return &self.0; } + let ext_sep = dir_sep + ext_sep_rel; + &self.0[..ext_sep] + }); + Self(format!("{without_ext}.{ext}")) + } + + pub fn basename(&self) -> &str { + self.0.trim_end_matches(|c| Self::path_predicate(c) || c == '.') + .rsplit_once(Self::path_predicate) + .map_or(&self.0, |(_, base)| base) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_with_extension() { + assert_eq!( + SimplePathBuf::from("name.ext").with_extension("new"), + SimplePathBuf::from("name.new")); + assert_eq!( + SimplePathBuf::from("/path/name.ext").with_extension("new"), + SimplePathBuf::from("/path/name.new")); + assert_eq!( + SimplePathBuf::from("/path.ext/name").with_extension("new"), + SimplePathBuf::from("/path.ext/name.new")); + assert_eq!( + SimplePathBuf::from("/path.ext/.name").with_extension("new"), + SimplePathBuf::from("/path.ext/.name.new")); + } + + #[test] + fn test_basename() { + assert_eq!(SimplePathBuf::from("name.ext").basename(), "name.ext"); + assert_eq!(SimplePathBuf::from("/path/name.ext").basename(), "name.ext"); + assert_eq!(SimplePathBuf::from("/path/name/").basename(), "name"); + assert_eq!(SimplePathBuf::from("/path/name/.").basename(), "name"); + assert_eq!(SimplePathBuf::from("/path/name/.//").basename(), "name"); + } +} diff --git a/jaarg/src/std.rs b/jaarg/src/std.rs index 2b5319b..a14f83b 100644 --- a/jaarg/src/std.rs +++ b/jaarg/src/std.rs @@ -5,8 +5,10 @@ extern crate std; -use crate::alloc::ParseMapResult; -use crate::{ErrorUsageWriter, ErrorUsageWriterContext, HandlerResult, HelpWriter, HelpWriterContext, Opt, Opts, ParseControl, ParseError, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter}; +use crate::{ + ErrorUsageWriter, ErrorUsageWriterContext, HandlerResult, HelpWriter, HelpWriterContext, Opt, Opts, + ParseControl, ParseError, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter, alloc::ParseMapResult +}; use std::path::Path; use std::rc::Rc; use std::{env, eprint, print};