1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
|
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`.
|