feat: add loading animation example

Add a busy-wait sleep on a monotonic clock and re-export String,
format!, and Duration from the library. The example animates a
progress bar with a spinner and logs each step in a Vec<String>.

Co-Authored-By: qwen (lmstudio/qwen3.8-27b@q3_k_xl): wrote example
This commit is contained in:
2026-09-05 05:09:50 +02:00
parent cd1c9ab97d
commit b7f9fefd78
5 changed files with 117 additions and 13 deletions
+11 -7
View File
@@ -6,16 +6,17 @@ This project is a small Rust common library and runtime built without Cargo.
CMake invokes `rustc` directly. The library provides a low-footprint subset CMake invokes `rustc` directly. The library provides a low-footprint subset
of standard-library-like functionality: of standard-library-like functionality:
- `Result` and formatting from `core` - `Result`, `Duration`, and formatting from `core`
- `Vec` and `vec![]` from `alloc` - `Vec`, `String`, `vec![]`, and `format!` from `alloc`
- project-local `print!` and `println!` macros - project-local `print!` and `println!` macros
- direct platform stdout and heap calls - direct platform stdout, heap calls, and busy-wait sleep
The project contains: The project contains:
- `src/lib.rs`: the `#![no_std]` common library API - `src/lib.rs`: the `#![no_std]` common library API
- `src/io.rs`: the `core::fmt::Write` stdout backend - `src/io.rs`: the `core::fmt::Write` stdout backend
- `src/runtime.rs`: allocator, async polling, and platform entry helpers - `src/runtime.rs`: allocator, async polling, monotonic sleep, and platform
entry helpers
- `macros/src/lib.rs`: host-side `#[rslibc_macros::main]` attribute macro - `macros/src/lib.rs`: host-side `#[rslibc_macros::main]` attribute macro
- `runtime/main.rs`: minimal example entry point - `runtime/main.rs`: minimal example entry point
- `CMakeLists.txt`: direct `rustc` library, macro, and executable commands - `CMakeLists.txt`: direct `rustc` library, macro, and executable commands
@@ -36,8 +37,8 @@ The project contains:
- Keep OS integration in small FFI modules. Do not pull in a general-purpose - Keep OS integration in small FFI modules. Do not pull in a general-purpose
runtime or C I/O layer for basic output. runtime or C I/O layer for basic output.
- `rslibc_macros::main` can wrap `async fn main()` with the small `block_on` - `rslibc_macros::main` can wrap `async fn main()` with the small `block_on`
executor. It is only a polling loop and does not provide Tokio-style I/O, executor. It is only a polling loop and does not provide Tokio-style I/O
timers, or a reactor. or a reactor; delays use the busy-wait `rslibc::runtime::sleep`.
- Keep unsafe code inside small safe wrappers. Application code should be - Keep unsafe code inside small safe wrappers. Application code should be
able to use calls such as: able to use calls such as:
@@ -52,6 +53,8 @@ The project contains:
- Windows stdout calls `GetStdHandle` and `WriteFile` through `kernel32`. - Windows stdout calls `GetStdHandle` and `WriteFile` through `kernel32`.
- Windows allocation calls `HeapAlloc` and `HeapFree` through the system - Windows allocation calls `HeapAlloc` and `HeapFree` through the system
heap. heap.
- Sleep uses a monotonic clock: `clock_gettime` on Unix and
`QueryPerformanceCounter` on Windows.
- The Windows runtime dynamically links only the platform support needed by - The Windows runtime dynamically links only the platform support needed by
`alloc` and the compiler runtime. Do not statically link the full C `alloc` and the compiler runtime. Do not statically link the full C
runtime. runtime.
@@ -116,7 +119,8 @@ cmake --build build
``` ```
On Windows, use `Remove-Item -Recurse -Force build` and run On Windows, use `Remove-Item -Recurse -Force build` and run
`build\\rslibc_example.exe` instead. Confirm that stdout contains `Hello`. `build\\rslibc_example.exe` instead. Confirm that stdout contains
`100% done`.
When size matters, inspect the Windows executable after a release build. When size matters, inspect the Windows executable after a release build.
Avoid adding formatting, allocation, or OS dependencies unless they are Avoid adding formatting, allocation, or OS dependencies unless they are
+5 -3
View File
@@ -8,7 +8,8 @@ library rlib, and a minimal example executable.
- `Result` and formatting from `core` - `Result` and formatting from `core`
- `Vec` and `vec![]` from `alloc` - `Vec` and `vec![]` from `alloc`
- project-local `print!` / `println!` macros - project-local `print!` / `println!` macros, plus `String` and `format!`
- busy-wait `sleep` on a monotonic clock
- direct platform stdout with no C I/O layer - direct platform stdout with no C I/O layer
- OS heap allocator (`malloc`/`free`, `HeapAlloc`/`HeapFree`) - OS heap allocator (`malloc`/`free`, `HeapAlloc`/`HeapFree`)
- `panic=abort` with a minimal panic handler - `panic=abort` with a minimal panic handler
@@ -41,8 +42,9 @@ Run the example:
.\build\rslibc_example.exe .\build\rslibc_example.exe
``` ```
The release example prints `Hello, World!`. The Windows executable is The release example runs an animated loading loop (progress bar plus
about 4 KiB and depends only on `KERNEL32.dll` and `VCRUNTIME140.dll`. spinner) and then prints a short log. The Windows executable is about
27 KiB and depends only on `KERNEL32.dll` and `VCRUNTIME140.dll`.
Individual targets: Individual targets:
+47 -3
View File
@@ -1,11 +1,55 @@
#![no_std] #![no_std]
#![no_main] #![no_main]
fn print(message: &str) { use core::time::Duration;
let _ = rslibc::io::write_str(message);
use rslibc::{format, print, println, vec, String, Vec};
const FRAMES: [&str; 4] = ["|", "/", "-", "\\"];
const WIDTH: usize = 20;
const STEPS: usize = 10;
fn bar(done: usize) -> String {
let mut out = String::new();
for i in 0..WIDTH {
out.push(if i < done { '#' } else { '-' });
}
out
}
fn load(step: usize) -> rslibc::Result<u32, &'static str> {
// Pretend to do work and return a checksum-like value.
let mut total = 0u32;
for value in vec![1, 2, 3, 4, 5] {
total = total.wrapping_add(value * step as u32);
}
Ok(total)
} }
#[rslibc_macros::main] #[rslibc_macros::main]
async fn main() { async fn main() {
print("Hello\n"); println!("rslibc loading example");
let mut log: Vec<String> = Vec::new();
for step in 0..STEPS {
match load(step) {
Ok(value) => {
let frame = FRAMES[step % FRAMES.len()];
let percent = 100 * (step + 1) / STEPS;
print!(
"\r [{}] {:>3}% {}",
bar((step + 1) * WIDTH / STEPS),
percent,
frame
);
log.push(format!("step {step} -> {value}"));
}
Err(message) => println!("error: {message}"),
}
rslibc::runtime::sleep(Duration::from_millis(60));
}
println!("\r [{}] 100% done", bar(WIDTH));
println!("log = {log:?}");
} }
+3
View File
@@ -5,9 +5,12 @@ extern crate alloc;
pub mod io; pub mod io;
pub mod runtime; pub mod runtime;
pub use alloc::format;
pub use alloc::string::String;
pub use alloc::vec; pub use alloc::vec;
pub use alloc::vec::Vec; pub use alloc::vec::Vec;
pub use core::result::Result; pub use core::result::Result;
pub use core::time::Duration;
#[macro_export] #[macro_export]
macro_rules! print { macro_rules! print {
+51
View File
@@ -2,11 +2,26 @@ use core::alloc::{GlobalAlloc, Layout};
use core::future::Future; use core::future::Future;
use core::pin::Pin; use core::pin::Pin;
use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use core::time::Duration;
#[cfg(unix)]
#[repr(C)]
struct Timespec {
tv_sec: i64,
tv_nsec: i64,
}
// CLOCK_MONOTONIC for the supported Unix targets.
#[cfg(all(unix, target_os = "macos"))]
const CLOCK_MONOTONIC: i32 = 4;
#[cfg(all(unix, not(target_os = "macos")))]
const CLOCK_MONOTONIC: i32 = 1;
#[cfg(unix)] #[cfg(unix)]
unsafe extern "C" { unsafe extern "C" {
fn malloc(size: usize) -> *mut u8; fn malloc(size: usize) -> *mut u8;
fn free(pointer: *mut u8); fn free(pointer: *mut u8);
fn clock_gettime(clock_id: i32, ts: *mut Timespec) -> i32;
} }
#[cfg(windows)] #[cfg(windows)]
@@ -15,6 +30,8 @@ unsafe extern "system" {
fn HeapAlloc(heap: *mut u8, flags: u32, bytes: usize) -> *mut u8; fn HeapAlloc(heap: *mut u8, flags: u32, bytes: usize) -> *mut u8;
fn HeapFree(heap: *mut u8, flags: u32, pointer: *mut u8) -> i32; fn HeapFree(heap: *mut u8, flags: u32, pointer: *mut u8) -> i32;
fn ExitProcess(status: u32) -> !; fn ExitProcess(status: u32) -> !;
fn QueryPerformanceCounter(counter: *mut i64) -> i32;
fn QueryPerformanceFrequency(frequency: *mut i64) -> i32;
} }
pub struct SystemAllocator; pub struct SystemAllocator;
@@ -71,6 +88,40 @@ pub fn exit(status: u32) -> ! {
unsafe { ExitProcess(status) } unsafe { ExitProcess(status) }
} }
fn now() -> Duration {
#[cfg(unix)]
{
let mut ts = Timespec { tv_sec: 0, tv_nsec: 0 };
if unsafe { clock_gettime(CLOCK_MONOTONIC, &mut ts) } == 0 && ts.tv_sec >= 0 {
return Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32);
}
Duration::ZERO
}
#[cfg(windows)]
{
let mut counter: i64 = 0;
let mut frequency: i64 = 0;
if unsafe { QueryPerformanceCounter(&mut counter) } != 0
&& unsafe { QueryPerformanceFrequency(&mut frequency) } != 0
&& frequency > 0
{
let nanos = (counter as u128 * 1_000_000_000u128) / frequency as u128;
return Duration::from_nanos(nanos as u64);
}
Duration::ZERO
}
}
// Busy-wait sleep on a monotonic clock. The executor has no reactor yet,
// so the platform cannot block us; spinning is the only correct option.
pub fn sleep(duration: Duration) {
let deadline = now() + duration;
while now() < deadline {
core::hint::spin_loop();
}
}
fn clone_waker(_: *const ()) -> RawWaker { fn clone_waker(_: *const ()) -> RawWaker {
RawWaker::new(core::ptr::null(), &WAKER_VTABLE) RawWaker::new(core::ptr::null(), &WAKER_VTABLE)
} }