From 251e3792717bb40dafc2c1ed72f686b840d65aa1 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sun, 1 Nov 2020 22:48:47 -0500 Subject: [PATCH] Add alloca helper to roc_std --- roc_std/.gitignore | 2 + roc_std/build.rs | 48 +++++++++++++++++++ roc_std/src/alloca.c | 9 ++++ roc_std/src/alloca.rs | 104 ++++++++++++++++++++++++++++++++++++++++++ roc_std/src/lib.rs | 2 + 5 files changed, 165 insertions(+) create mode 100644 roc_std/.gitignore create mode 100644 roc_std/build.rs create mode 100644 roc_std/src/alloca.c create mode 100644 roc_std/src/alloca.rs diff --git a/roc_std/.gitignore b/roc_std/.gitignore new file mode 100644 index 0000000000..e0292b19e0 --- /dev/null +++ b/roc_std/.gitignore @@ -0,0 +1,2 @@ +*.o +*.a diff --git a/roc_std/build.rs b/roc_std/build.rs new file mode 100644 index 0000000000..6625c9c3de --- /dev/null +++ b/roc_std/build.rs @@ -0,0 +1,48 @@ +// Adapted from https://github.com/TheDan64/scoped_alloca +// by Daniel Kolsoi, licensed under the Apache License 2.0 +// Thank you, Dan! + +use std::env; +use std::fs::create_dir; +use std::path::Path; +use std::process::Command; + +fn main() { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let cargo_dir = Path::new(manifest_dir.as_str()); + let lib_dir = cargo_dir.join("lib"); + let alloca_c = cargo_dir.join("src/alloca.c"); + let alloca_o = lib_dir.join("alloca.o"); + let liballoca_a = lib_dir.join("liballoca.a"); + + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + + // No need to recompile alloca static lib every time. We could + // add a feature flag to do so if needed, though + if liballoca_a.is_file() { + return; + } + + if !lib_dir.is_dir() { + create_dir(&lib_dir).unwrap(); + } + + let clang_output = Command::new("clang") + .arg("-c") + .arg(alloca_c) + .arg("-o") + .arg(&alloca_o) + .output() + .expect("Could not execute clang"); + + assert!(clang_output.status.success(), "{:?}", clang_output); + + let ar_output = Command::new("ar") + .arg("-q") + .arg(liballoca_a) + .arg(alloca_o) + .output() + .expect("Could not execute ar"); + + assert!(ar_output.status.success(), "{:?}", ar_output); +} diff --git a/roc_std/src/alloca.c b/roc_std/src/alloca.c new file mode 100644 index 0000000000..e8bf7ee670 --- /dev/null +++ b/roc_std/src/alloca.c @@ -0,0 +1,9 @@ +#include + +// From https://github.com/TheDan64/scoped_alloca +// by Daniel Kolsoi, licensed under the Apache License 2.0 +// Thank you, Dan! + +void *c_alloca(size_t bytes) { + return alloca(bytes); +} diff --git a/roc_std/src/alloca.rs b/roc_std/src/alloca.rs new file mode 100644 index 0000000000..52322d2d8a --- /dev/null +++ b/roc_std/src/alloca.rs @@ -0,0 +1,104 @@ +// Adapted from https://github.com/TheDan64/scoped_alloca +// by Daniel Kolsoi, licensed under the Apache License 2.0 +// Thank you, Dan! + +use libc::{c_void, size_t}; + +#[link(name = "alloca")] +extern "C" { + #[no_mangle] + fn c_alloca(_: size_t) -> *mut c_void; +} + +/// This calls C's `alloca` function to allocate some bytes on the stack, +/// then provides those bytes to the given callback function. +/// +/// Note that this function is `#[inline(always)]`, which means it will allocate +/// the bytes on the stack of whoever calls this function. +/// +/// Naturally, if you give this a large number of bytes, it may cause +/// stack overflows, so be careful! +/// +/// Due to how Rust FFI works with inlining, in debug builds this actually +/// allocates on the heap (using `malloc`) and then deallocates the memory +/// (using `free`) before the callback returns. In debug builds, this can lead +/// to memory leaks if the callback panics - but release builds should be fine, +/// because they only ever allocate on the stack. +#[inline(always)] +pub unsafe fn with_stack_bytes(bytes: usize, callback: F) -> R +where + F: FnOnce(*mut c_void) -> R, +{ + let ptr = malloc_or_alloca(bytes); + let answer = callback(ptr); + + free_or_noop(ptr); + + answer +} + +#[cfg(debug_assertions)] +#[inline(always)] +unsafe fn malloc_or_alloca(bytes: usize) -> *mut c_void { + libc::malloc(bytes) +} + +#[cfg(not(debug_assertions))] +#[inline(always)] +unsafe fn malloc_or_alloca(bytes: usize) -> *mut c_void { + c_alloca(bytes) +} + +#[cfg(debug_assertions)] +#[inline(always)] +unsafe fn free_or_noop(ptr: *mut c_void) { + libc::free(ptr) +} + +#[cfg(not(debug_assertions))] +#[inline(always)] +fn free_or_noop(_ptr: *mut c_void) { + // In release builds, we'll have used alloca, + // so there's nothing to free. +} + +#[cfg(test)] +mod tests { + use super::with_stack_bytes; + use core::mem::size_of; + + #[repr(C)] + #[derive(Debug, PartialEq)] + struct TestStruct { + x: u8, + y: u16, + z: u64, + } + + #[test] + fn test_alloca() { + let test_struct = TestStruct { + x: 123, + y: 4567, + z: 89012345678, + }; + let sum: u64 = unsafe { + with_stack_bytes(size_of::(), |ptr| { + let ptr = ptr as *mut TestStruct; + + (*ptr).x = test_struct.x; + (*ptr).y = test_struct.y; + (*ptr).z = test_struct.z; + + assert_eq!(*ptr, test_struct); + + (*ptr).x as u64 + (*ptr).y as u64 + (*ptr).z as u64 + }) + }; + + assert_eq!( + sum, + test_struct.x as u64 + test_struct.y as u64 + test_struct.z as u64 + ); + } +} diff --git a/roc_std/src/lib.rs b/roc_std/src/lib.rs index 177ba87e99..10e93386de 100644 --- a/roc_std/src/lib.rs +++ b/roc_std/src/lib.rs @@ -2,6 +2,8 @@ #![no_std] use core::fmt; +pub mod alloca; + // A list of C functions that are being imported extern "C" { pub fn printf(format: *const u8, ...) -> i32;