Create no_std examples

This commit is contained in:
2025-11-07 06:41:34 +11:00
parent 304e12bd8e
commit ec0f3f0739
11 changed files with 445 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
[workspace]
members = ["jaarg"]
default-members = ["jaarg"]
members = ["jaarg-nostd"]
resolver = "3"
[workspace.package]

View File

@@ -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.

View File

@@ -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"

10
jaarg-nostd/Cargo.toml Normal file
View File

@@ -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"]

4
jaarg-nostd/README.md Normal file
View File

@@ -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.

View File

@@ -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<SimplePathBuf> = None;
let mut number = 0;
// Set up arguments table
enum Arg { Help, Number, File, Out }
const OPTIONS: Opts<Arg> = 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
}

View File

@@ -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
}

210
jaarg-nostd/src/harness.rs Normal file
View File

@@ -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<usize> = 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<usize> = Some(16);
// if !(ptr == 32 || ptr == 64)
#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
const ALIGNMENT: Option<usize> = 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::<u8>()
} else {
#[cfg(target_family = "windows")]
return c::aligned_alloc(layout.size(), layout.align()).cast::<u8>();
#[cfg(not(target_family = "windows"))]
return c::aligned_alloc(layout.align(), layout.size()).cast::<u8>();
}
}
#[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);
}
}

6
jaarg-nostd/src/lib.rs Normal file
View File

@@ -0,0 +1,6 @@
#![no_std]
extern crate alloc;
pub mod harness;
pub mod simplepathbuf;

View File

@@ -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<S: AsRef<str>> From<S> for SimplePathBuf where String: From<S> {
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");
}
}

View File

@@ -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};