add: compiler
This commit is contained in:
21
rpgrt/Cargo.toml
Normal file
21
rpgrt/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "rpgrt"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Build as a C-compatible shared library (librpgrt.so) so that RPG IV programs
|
||||
# compiled by rust-langrpg can link against it at runtime.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[lib]
|
||||
name = "rpgrt"
|
||||
path = "src/lib.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
# cdylib → produces librpgrt.so (loaded by compiled RPG binaries)
|
||||
# rlib → allows `cargo test` to run the unit tests in src/lib.rs
|
||||
|
||||
[dependencies]
|
||||
# No external dependencies — the runtime is intentionally minimal and relies
|
||||
# only on the Rust standard library and libc (linked automatically).
|
||||
496
rpgrt/src/lib.rs
Normal file
496
rpgrt/src/lib.rs
Normal file
@@ -0,0 +1,496 @@
|
||||
//! rpgrt.rs — RPG IV runtime library.
|
||||
//!
|
||||
//! This crate is compiled as a C-compatible shared library (`librpgrt.so`) that
|
||||
//! RPG IV programs compiled by `rust-langrpg` link against at runtime.
|
||||
//!
|
||||
//! ## Exported symbols
|
||||
//!
|
||||
//! | Symbol | Signature | Description |
|
||||
//! |---------------------|----------------------------------------|--------------------------------------|
|
||||
//! | `rpg_dsply` | `(ptr: *const u8, len: i64)` | Display a fixed-length char field |
|
||||
//! | `rpg_dsply_cstr` | `(ptr: *const c_char)` | Display a null-terminated C string |
|
||||
//! | `rpg_dsply_i64` | `(n: i64)` | Display a signed 64-bit integer |
|
||||
//! | `rpg_dsply_f64` | `(f: f64)` | Display a double-precision float |
|
||||
//! | `rpg_halt` | `(code: i32)` | Abnormal program termination |
|
||||
//!
|
||||
//! ## Building
|
||||
//!
|
||||
//! The runtime is built automatically by `build.rs` as part of the normal
|
||||
//! `cargo build` invocation. The resulting `librpgrt.so` is placed in the
|
||||
//! Cargo output directory and the compiler binary links executables against it.
|
||||
//!
|
||||
//! To build it standalone:
|
||||
//!
|
||||
//! ```sh
|
||||
//! rustc --edition 2024 --crate-type cdylib -o librpgrt.so src/bin/rpgrt.rs
|
||||
//! ```
|
||||
//!
|
||||
//! ## DSPLY semantics
|
||||
//!
|
||||
//! In a real IBM i system, `DSPLY` writes a message to the *program message
|
||||
//! queue* (an interactive operator message queue). On a Linux host there is no
|
||||
//! equivalent facility, so we write to **stdout**, mirroring the message format
|
||||
//! IBM i uses:
|
||||
//!
|
||||
//! ```text
|
||||
//! DSPLY Hello, World!
|
||||
//! ```
|
||||
//!
|
||||
//! The output is flushed immediately after every `DSPLY` call, matching the
|
||||
//! interactive behaviour of the IBM i runtime.
|
||||
//!
|
||||
//! Trailing ASCII spaces (0x20) are stripped from fixed-length `CHAR` fields
|
||||
//! before display, exactly as IBM i does.
|
||||
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::io::{self, Write};
|
||||
use std::slice;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_dsply — display a fixed-length character field
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Display the first `len` bytes pointed to by `ptr`, trimming trailing spaces,
|
||||
/// then print a newline and flush stdout.
|
||||
///
|
||||
/// This is the primary entry point called by the LLVM-compiled RPG procedure
|
||||
/// for `DSPLY variable_name;`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// * `ptr` must be valid for at least `len` bytes.
|
||||
/// * `len` must be ≥ 0. A negative `len` is silently treated as 0.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_dsply(ptr: *const u8, len: i64) {
|
||||
let bytes = if ptr.is_null() || len <= 0 {
|
||||
b"" as &[u8]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(ptr, len as usize) }
|
||||
};
|
||||
|
||||
// Strip trailing spaces (IBM i CHAR fields are space-padded to their
|
||||
// declared length).
|
||||
let trimmed = rtrim_spaces(bytes);
|
||||
|
||||
// Convert to a lossy UTF-8 string so non-ASCII EBCDIC-origin data at
|
||||
// least renders something printable rather than crashing.
|
||||
let text = String::from_utf8_lossy(trimmed);
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
// Mimic IBM i DSPLY prefix.
|
||||
let _ = writeln!(out, "DSPLY {}", text);
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_dsply_cstr — display a null-terminated C string
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Display a null-terminated C string with a `DSPLY` prefix.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `ptr` must point to a valid null-terminated C string.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_dsply_cstr(ptr: *const std::os::raw::c_char) {
|
||||
let text = if ptr.is_null() {
|
||||
std::borrow::Cow::Borrowed("")
|
||||
} else {
|
||||
unsafe { CStr::from_ptr(ptr).to_string_lossy() }
|
||||
};
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
let _ = writeln!(out, "DSPLY {}", text);
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_dsply_i64 — display a signed 64-bit integer
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Display the decimal representation of a signed 64-bit integer.
|
||||
///
|
||||
/// Used when the argument to `DSPLY` is an integer expression rather than a
|
||||
/// character variable.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rpg_dsply_i64(n: i64) {
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
let _ = writeln!(out, "DSPLY {}", n);
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_dsply_f64 — display a double-precision float
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Display the decimal representation of a 64-bit IEEE 754 float.
|
||||
///
|
||||
/// Matches the numeric formatting IBM i uses for packed-decimal fields when
|
||||
/// displayed via `DSPLY`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rpg_dsply_f64(f: f64) {
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
// Format with enough precision to round-trip.
|
||||
let _ = writeln!(out, "DSPLY {}", f);
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_halt — abnormal termination
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Terminate the program with the given exit code after printing an error
|
||||
/// banner to stderr.
|
||||
///
|
||||
/// Maps roughly to the IBM i concept of an *unhandled exception* ending the
|
||||
/// job.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rpg_halt(code: i32) {
|
||||
eprintln!("RPG program halted with code {}", code);
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_memset_char — fill a CHAR field with a repeated byte
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Fill the first `len` bytes at `ptr` with `fill_byte`.
|
||||
///
|
||||
/// Used by `CLEAR` and `RESET` for character fields (fill with space 0x20).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `ptr` must be valid for at least `len` bytes and must be writable.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_memset_char(ptr: *mut u8, fill_byte: u8, len: i64) {
|
||||
if ptr.is_null() || len <= 0 {
|
||||
return;
|
||||
}
|
||||
let slice = unsafe { slice::from_raw_parts_mut(ptr, len as usize) };
|
||||
slice.fill(fill_byte);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_move_char — move (copy) a CHAR field, padding / truncating as needed
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Copy `src_len` bytes from `src` into a `dst_len`-byte field at `dst`.
|
||||
///
|
||||
/// * If `src_len` < `dst_len` the destination is right-padded with spaces.
|
||||
/// * If `src_len` > `dst_len` only the first `dst_len` bytes of `src` are
|
||||
/// copied (left-truncation rule, matching RPG IV `MOVE` semantics).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Both `src` and `dst` must be valid for their respective lengths.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_move_char(
|
||||
dst: *mut u8,
|
||||
dst_len: i64,
|
||||
src: *const u8,
|
||||
src_len: i64,
|
||||
) {
|
||||
if dst.is_null() || src.is_null() || dst_len <= 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let dst_slice = unsafe { slice::from_raw_parts_mut(dst, dst_len as usize) };
|
||||
let copy_len = (src_len.min(dst_len)) as usize;
|
||||
|
||||
if src_len > 0 {
|
||||
let src_slice = unsafe { slice::from_raw_parts(src, src_len as usize) };
|
||||
dst_slice[..copy_len].copy_from_slice(&src_slice[..copy_len]);
|
||||
}
|
||||
|
||||
// Pad remainder with spaces.
|
||||
if (copy_len as i64) < dst_len {
|
||||
dst_slice[copy_len..].fill(b' ');
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_trim — return pointer + new length for a space-trimmed CHAR field
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Write the trimmed start pointer and trimmed length of a CHAR field into
|
||||
/// `out_ptr` and `out_len` respectively.
|
||||
///
|
||||
/// Leading *and* trailing spaces are stripped (equivalent to `%TRIM`).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// * `ptr` must be valid for `len` bytes.
|
||||
/// * `out_ptr` and `out_len` must be valid writable pointers.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_trim(
|
||||
ptr: *const u8,
|
||||
len: i64,
|
||||
out_ptr: *mut *const u8,
|
||||
out_len: *mut i64,
|
||||
) {
|
||||
if ptr.is_null() || len <= 0 || out_ptr.is_null() || out_len.is_null() {
|
||||
if !out_ptr.is_null() { unsafe { *out_ptr = ptr; } }
|
||||
if !out_len.is_null() { unsafe { *out_len = 0; } }
|
||||
return;
|
||||
}
|
||||
|
||||
let bytes = unsafe { slice::from_raw_parts(ptr, len as usize) };
|
||||
let trimmed = bytes
|
||||
.iter()
|
||||
.position(|&b| b != b' ')
|
||||
.map(|start| {
|
||||
let end = bytes.iter().rposition(|&b| b != b' ').unwrap_or(start) + 1;
|
||||
&bytes[start..end]
|
||||
})
|
||||
.unwrap_or(&bytes[0..0]);
|
||||
|
||||
unsafe {
|
||||
*out_ptr = trimmed.as_ptr();
|
||||
*out_len = trimmed.len() as i64;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_len — return the non-space length of a CHAR field (%LEN semantics)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Return the *declared* length of a CHAR field (i.e. `len` itself), not the
|
||||
/// trimmed length. This matches RPG IV `%LEN` which returns the declared size.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// No pointer dereference is performed; this function is trivially safe.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn rpg_len(_ptr: *const u8, len: i64) -> i64 {
|
||||
len
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_scan — %SCAN(search : source [: start])
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Search for `search_ptr[0..search_len]` inside `src_ptr[0..src_len]`
|
||||
/// starting at byte offset `start` (1-based, RPG IV convention).
|
||||
///
|
||||
/// Returns the 1-based position of the first match, or 0 if not found.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Both pointers must be valid for their respective lengths.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_scan(
|
||||
search_ptr: *const u8,
|
||||
search_len: i64,
|
||||
src_ptr: *const u8,
|
||||
src_len: i64,
|
||||
start: i64, // 1-based; 0 means "from beginning" (treated as 1)
|
||||
) -> i64 {
|
||||
if search_ptr.is_null() || src_ptr.is_null()
|
||||
|| search_len <= 0 || src_len <= 0
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
let needle = unsafe { slice::from_raw_parts(search_ptr, search_len as usize) };
|
||||
let hay = unsafe { slice::from_raw_parts(src_ptr, src_len as usize) };
|
||||
|
||||
let from = if start <= 0 { 0 } else { (start - 1) as usize };
|
||||
if from >= hay.len() { return 0; }
|
||||
|
||||
hay[from..]
|
||||
.windows(needle.len())
|
||||
.position(|w| w == needle)
|
||||
.map(|p| (from + p + 1) as i64) // convert back to 1-based
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_subst — %SUBST(str : start [: len])
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Write up to `sub_len` bytes from `src_ptr` starting at byte `start`
|
||||
/// (1-based) into `dst_ptr`. Returns the number of bytes written.
|
||||
///
|
||||
/// If `sub_len` is 0 the function copies from `start` to the end of the
|
||||
/// source field (mirrors RPG IV `%SUBST` two-argument form).
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// All pointers must be valid for their respective lengths.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_subst(
|
||||
src_ptr: *const u8,
|
||||
src_len: i64,
|
||||
start: i64, // 1-based
|
||||
sub_len: i64, // 0 = "to end"
|
||||
dst_ptr: *mut u8,
|
||||
dst_len: i64,
|
||||
) -> i64 {
|
||||
if src_ptr.is_null() || dst_ptr.is_null() || src_len <= 0 || dst_len <= 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let src = unsafe { slice::from_raw_parts(src_ptr, src_len as usize) };
|
||||
let dst = unsafe { slice::from_raw_parts_mut(dst_ptr, dst_len as usize) };
|
||||
|
||||
let from = if start <= 1 { 0 } else { (start - 1) as usize };
|
||||
if from >= src.len() { return 0; }
|
||||
|
||||
let available = src.len() - from;
|
||||
let want = if sub_len <= 0 {
|
||||
available
|
||||
} else {
|
||||
(sub_len as usize).min(available)
|
||||
};
|
||||
let copy = want.min(dst.len());
|
||||
|
||||
dst[..copy].copy_from_slice(&src[from..from + copy]);
|
||||
copy as i64
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helper: trim trailing ASCII spaces
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[inline]
|
||||
fn rtrim_spaces(bytes: &[u8]) -> &[u8] {
|
||||
let end = bytes
|
||||
.iter()
|
||||
.rposition(|&b| b != b' ')
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
&bytes[..end]
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rtrim_strips_spaces() {
|
||||
assert_eq!(rtrim_spaces(b"hello "), b"hello");
|
||||
assert_eq!(rtrim_spaces(b"hello"), b"hello");
|
||||
assert_eq!(rtrim_spaces(b" "), b"");
|
||||
assert_eq!(rtrim_spaces(b""), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtrim_preserves_internal_spaces() {
|
||||
assert_eq!(rtrim_spaces(b"hello world "), b"hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_scan_finds_match() {
|
||||
let hay = b"Hello, World!";
|
||||
let needle = b"World";
|
||||
let pos = unsafe {
|
||||
rpg_scan(
|
||||
needle.as_ptr(), needle.len() as i64,
|
||||
hay.as_ptr(), hay.len() as i64,
|
||||
1,
|
||||
)
|
||||
};
|
||||
assert_eq!(pos, 8); // 1-based position of 'W'
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_scan_not_found() {
|
||||
let hay = b"Hello";
|
||||
let needle = b"XYZ";
|
||||
let pos = unsafe {
|
||||
rpg_scan(
|
||||
needle.as_ptr(), needle.len() as i64,
|
||||
hay.as_ptr(), hay.len() as i64,
|
||||
1,
|
||||
)
|
||||
};
|
||||
assert_eq!(pos, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_subst_copies_correctly() {
|
||||
let src = b"Hello, World!";
|
||||
let mut dst = vec![0u8; 5];
|
||||
let written = unsafe {
|
||||
rpg_subst(
|
||||
src.as_ptr(), src.len() as i64,
|
||||
8, // start at 'W' (1-based)
|
||||
5,
|
||||
dst.as_mut_ptr(), dst.len() as i64,
|
||||
)
|
||||
};
|
||||
assert_eq!(written, 5);
|
||||
assert_eq!(&dst, b"World");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_move_char_pads_with_spaces() {
|
||||
let src = b"Hi";
|
||||
let mut dst = vec![0u8; 5];
|
||||
unsafe {
|
||||
rpg_move_char(
|
||||
dst.as_mut_ptr(), dst.len() as i64,
|
||||
src.as_ptr(), src.len() as i64,
|
||||
);
|
||||
}
|
||||
assert_eq!(&dst, b"Hi ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_move_char_truncates() {
|
||||
let src = b"Hello, World!";
|
||||
let mut dst = vec![0u8; 5];
|
||||
unsafe {
|
||||
rpg_move_char(
|
||||
dst.as_mut_ptr(), dst.len() as i64,
|
||||
src.as_ptr(), src.len() as i64,
|
||||
);
|
||||
}
|
||||
assert_eq!(&dst, b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_trim_removes_leading_and_trailing() {
|
||||
let input = b" hello ";
|
||||
let mut out_ptr: *const u8 = std::ptr::null();
|
||||
let mut out_len: i64 = 0;
|
||||
unsafe {
|
||||
rpg_trim(
|
||||
input.as_ptr(), input.len() as i64,
|
||||
&mut out_ptr, &mut out_len,
|
||||
);
|
||||
let result = std::slice::from_raw_parts(out_ptr, out_len as usize);
|
||||
assert_eq!(result, b"hello");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_dsply_smoke() {
|
||||
// Just ensure it doesn't panic.
|
||||
let msg = b"Hello, World! ";
|
||||
unsafe { rpg_dsply(msg.as_ptr(), msg.len() as i64) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_dsply_i64_smoke() {
|
||||
rpg_dsply_i64(42);
|
||||
rpg_dsply_i64(-1);
|
||||
rpg_dsply_i64(i64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpg_dsply_f64_smoke() {
|
||||
rpg_dsply_f64(3.14159);
|
||||
rpg_dsply_f64(-0.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user