apr 0.4.0

Rust bindings for Apache Portable Runtime
Documentation
Short version: keep APR pools and raw pointers completely inside your crate; present a safe, idiomatic API with RAII, Result, and normal Rust ownership. Treat a pool as a scoped, non-Send arena whose lifetime bounds any borrowed data you hand out. Convert to owned Rust types at API edges unless the caller opts into zero-copy “borrowed from pool” views.

Here’s a blueprint that’s worked well for C libs with arena allocators (APR) and callback batons (Subversion):

1) Crate layout

apr-sys and svn-sys: raw bindgen outputs + build.rs that uses pkg-config (apr-1, aprutil-1, svn_client-1, svn_fs-1, …).

apr: tiny safe wrappers for pools, time, tables, hashes, files/streams.

svn: the public API (client, wc, ra, fs). Re-export just enough types.

Keep -sys crates separate so consumers with exotic link setups can swap them.

2) Initialization

APR requires one-time init which is handled automatically via ctor in this crate.

3) APR pool wrapper (hidden by default)

Model pools as scoped arenas. They are not thread-safe and most APR/SVN allocations are tied to them. Provide:

#[repr(transparent)]
pub struct Pool {
    raw: *mut apr_sys::apr_pool_t,
    // !Send + !Sync:
    _no_send: std::marker::PhantomData<*mut ()>,
}

impl Pool {
    pub fn new() -> Result<Self, AprError> { /* create root */ }
    pub fn with_child<R>(&self, f: impl FnOnce(&Pool) -> R) -> R { /* apr_pool_create_ex */ }
}

impl Drop for Pool { fn drop(&mut self){ unsafe { apr_sys::apr_pool_destroy(self.raw) } } }


But: do not expose Pool in your top-level svn API unless an “advanced/zero-copy” feature is enabled. Normal users shouldn’t think about pools.

Also provide a helper:

pub(crate) fn with_tmp_pool<R>(f: impl FnOnce(&Pool) -> R) -> R { /* root->child->destroy */ }


Use this around each FFI call so temporary allocations vanish deterministically.

4) Lifetimes: owned vs borrowed

Default: copy C data into Rust-owned types (String, Vec<u8>, PathBuf, SystemTime).

Advanced: offer borrowed views tied to a Pool lifetime:

pub struct BStr<'a>(&'a [u8]); // bytes from svn_string_t
pub struct BStrUtf8<'a>(&'a str); // if you validate UTF-8

pub struct DirEntry<'a> {
    pub name: BStrUtf8<'a>,
    pub kind: NodeKind,
    // …
    _pool: PhantomData<&'a Pool>,
}


Inside, convert:

fn svn_string_as_bytes<'a>(s: *const svn_sys::svn_string_t) -> BStr<'a> { /* from data/len */ }


Never return references that outlive the pool scope.

5) Error handling

Subversion uses an error chain (svn_error_t). Convert to a single Rust error that preserves code and a joined message:

#[derive(thiserror::Error, Debug)]
#[error("{message}")]
pub struct SvnError {
    pub code: i32,
    pub message: String,
}

impl From<*mut svn_sys::svn_error_t> for SvnError {
    fn from(mut e: *mut svn_sys::svn_error_t) -> Self {
        let mut msgs = Vec::new();
        let code = unsafe { (*e).apr_err };
        while !e.is_null() {
            unsafe {
                let cstr = std::ffi::CStr::from_ptr((*e).message);
                msgs.push(cstr.to_string_lossy().into_owned());
                e = (*e).child;
            }
        }
        unsafe { svn_sys::svn_error_clear(e) } // clear chain
        Self { code, message: msgs.join(": ") }
    }
}


Return Result<T, SvnError> everywhere.

6) Strings, paths, and encodings

svn_string_t/svn_stringbuf_t are byte sequences. Treat as &[u8] and validate UTF-8 only when needed.

Paths: Subversion is byte-oriented; on Windows you’ll need normalization. Public API should take impl AsRef<Path> and convert to platform encoding inside the pool with svn_dirent_internal_style/svn_utf__* helpers as needed, but hand back PathBuf on Rust side.

7) RAII wrappers for core handles

Make thin, repr(transparent) newtypes with Drop:

#[repr(transparent)]
pub struct ClientCtx(*mut svn_sys::svn_client_ctx_t);
impl ClientCtx {
    pub fn new() -> Result<Self, SvnError> {
        with_tmp_pool(|p| unsafe {
            let mut ctx = std::ptr::null_mut();
            svn_result(svn_sys::svn_client_create_context2(&mut ctx, std::ptr::null(), p.raw))?;
            Ok(ClientCtx(ctx))
        })
    }
}
impl Drop for ClientCtx {
    fn drop(&mut self) { /* nothing; ctx entries are pool-owned */ }
}


For handles whose lifetime is pool-bound, keep the pool inside the wrapper so it can’t outlive it:

pub struct Client {
    ctx: ClientCtx,
    pool: Pool, // root keeping ctx alive
}

8) Cancel funcs, prompts, and other callbacks

Expose Rust closures; store them in a Box and pass as a baton pointer. Provide an extern "C" trampoline:

type CancelFn = dyn FnMut() -> bool + Send; // or not Send

extern "C" fn cancel_trampoline(baton: *mut std::ffi::c_void) -> svn_sys::svn_error_t_ptr {
    let f = unsafe { &mut *(baton as *mut Box<CancelFn>) };
    if f() { std::ptr::null_mut() } else { /* return SVN_ERR_CANCELLED */ }
}

pub struct CancelHandle {
    _boxed: Box<CancelFn>, // kept alive
    baton: *mut c_void,
}


Do the same for log receivers and RA callbacks. Document reentrancy and threading.

9) Public API sketch (owned by default)
pub struct Client { /* holds ctx + root pool */ }

impl Client {
    pub fn new() -> Result<Self, SvnError> { /* … */ }

    pub fn checkout(
        &self,
        url: &str,
        dst: impl AsRef<std::path::Path>,
        rev: Revision,            // enum { Head, Number(i64), Date(SystemTime) }
        depth: Depth,             // enum mapping svn_depth_t
        opts: CheckoutOpts,       // builder for ignore externals, etc.
    ) -> Result<CheckoutReport, SvnError> {
        with_tmp_pool(|p| unsafe {
            // convert inputs into pool allocations
            // call svn_client_checkout3
            // gather outputs as owned Rust types
        })
    }

    pub fn log(
        &self,
        path_or_url: &str,
        range: RevRange,
        mut receiver: impl FnMut(&LogEntry) -> ControlFlow<()> // stop early
    ) -> Result<(), SvnError> {
        // set up baton + trampoline + tmp pool per callback invocation, if needed
    }
}


Provide *_borrowed variants behind a feature that return LogEntry<'pool> etc. for high-perf traversals.

10) Concurrency and Send/Sync

Mark pool-bound structs as !Send and !Sync (use a PhantomData<*mut ()>).

It’s okay for high-level Client to be Send if you serialize operations internally or keep separate temporary pools per method call (APR pools aren’t thread-safe).

Document: “You may call methods from multiple threads, but each method call is single-threaded and uses its own temporary pool.”

11) Time and numbers

apr_time_t is microseconds since Unix epoch. Convert with care:

pub fn to_system_time(t: apr_sys::apr_time_t) -> std::time::SystemTime {
    std::time::UNIX_EPOCH + std::time::Duration::from_micros(t as u64)
}

12) Testing strategy

Spin up a throwaway repo in a temp dir via svnadmin create and file:// URLs for integration tests.

CI matrix across Linux/macOS/Windows; link with system Subversion via pkg-config.

Fuzz parsers of svn_opt_revision_t etc. with arbitrary/proptest.

13) Practical pitfalls (watch out!)

Pool lifetime leaks: never stash pool-allocated pointers in Rust structs that can outlive the pool. Either copy or tie with a lifetime.

Error chains: always call svn_error_clear after converting, or you’ll leak.

UTF-8 assumptions: log messages and author names are not guaranteed UTF-8. Validate/lossy.

APR tables/hashes: wrap as iterators that copy keys/values; borrowed variants must carry the pool lifetime.

Windows paths: normalize and use OsString↔byte conversions carefully.

Minimal code slice: one safe call end-to-end
pub fn svn_result(code: svn_sys::svn_error_t_ptr) -> Result<(), SvnError> {
    if code.is_null() { Ok(()) } else { Err(SvnError::from(code)) }
}

impl Client {
    pub fn username(&self) -> Result<Option<String>, SvnError> {
        with_tmp_pool(|p| unsafe {
            let mut name: *const std::os::raw::c_char = std::ptr::null();
            svn_result(svn_sys::svn_client_get_username(&mut name, self.ctx.0, p.raw))?;
            if name.is_null() { Ok(None) }
            else {
                Ok(Some(std::ffi::CStr::from_ptr(name).to_string_lossy().into_owned()))
            }
        })
    }
}

TL;DR design rules

Hide pools by default.

Tie any borrowed data to a pool lifetime; otherwise copy.

Wrap every handle in RAII; mark pool-bound types !Send/!Sync.

Convert svn_error_t chains to a single Error.

Use scoped temporary pools per call; keep a root pool to own long-lived handles like svn_client_ctx_t.

Expose callbacks via closures with boxed batons + extern "C" trampolines.

If you want, I can sketch a tiny svn::Client::checkout implementation skeleton you can drop into a new crate (with build.rs/pkg-config ready).

Svn-rs - Checkout Skeleton
· rust
            let depth = svn_sys::svn_depth_infinity;
            let ignore_externals = 0; // boolean


            let err = svn_sys::svn_client_checkout3(
                &mut result_rev as *mut _,
                url_c.as_ptr(),
                dst_c.as_ptr(),
                ptr::null(), // peg revision
                ptr::null(), // revision
                depth,
                ignore_externals,
                ptr::null_mut(), // ctx - in real code, pass self.ctx
                p.raw,
            );


            if !err.is_null() {
                // convert error and clear it
                let e = SvnError::from(err);
                svn_sys::svn_error_clear(err);
                return Err(e);
            }


            Ok(CheckoutReport { revision: result_rev })
        })
    }
}


impl Drop for Client {
    fn drop(&mut self) {
        // in real code, nothing explicit required if ctx is pool-owned; pool Drop will clean up
    }
}


// ---------- Small return types --------------------------------------------
pub struct CheckoutReport {
    pub revision: svn_sys::svn_revnum_t,
}


// ------------------------------- Notes ------------------------------------
// - This is a sketch to show the pattern: create short-lived pools per FFI call, keep a root
//   pool inside long-lived handles, convert svn errors to Rust Results, and copy C-owned
//   data to Rust-owned types at API boundaries.
// - A production crate should:
//   * Arrange for svn_client_ctx_t to be allocated in the Client.root pool.
//   * Carefully handle path encoding on Windows.
//   * Provide borrowed variants (tied to pool lifetimes) behind a feature flag.
//   * Provide safe wrappers for callbacks using boxed closures and trampolines.
//   * Add proper SVN/APR initialization and thread cleanup.


// Cargo/build notes (not included programmatically here):
// - link to system apr and subversion libraries with pkg-config in build.rs.
// - generate bindings with bindgen and expose them in `svn-sys` and `apr-sys`.