Rust

43 Posts

Publishing CLI tools, crates, and versions

6 minute

When a C project is shipped to other people, the hard part is often not whether the source builds. The hard part is the contract around it:

How is the tool installed into PATH?
Where do the headers and libraries go?
Which version should users link against?
Will an upgrade break existing code?
Does a public API change count as compatible?
Can the binary ABI remain stable over time?

Rust has the same questions, but the shipped object usually falls into one of three groups:

Read More

release, cross compilation, and binary size

6 minute

Before delivering a C project, “it compiles” is not enough.

You still check:

whether release optimization is enabled
whether architecture and ABI match the target
whether linked libraries are target libraries or host libraries
whether the binary carries unnecessary symbols
whether runtime dynamic dependencies exist on the target
whether size fits device limits
whether crashes can still be symbolized

Rust delivery for CLI tools, system libraries, or C/Rust boundary artifacts has the same concerns. Cargo simplifies the build command, but it does not remove release checks.

Read More

workspace, features, and build configuration

6 minute

As C projects grow, they often accumulate:

several static libraries and command-line tools
public and internal headers
Makefile subdirectories
CONFIG_XXX / #ifdef switches
Debug/Release compiler flags
target platform differences

Rust projects face the same issues with different tools:

workspace -> manages several crates
crate     -> boundary for a library or executable target
feature   -> optional capabilities and optional dependencies
profile   -> debug/release build parameters
cfg       -> platform and conditional compilation

This article does not cover every Cargo feature. It focuses on the organization needed for libraries, CLI tools, and C/Rust boundaries.

Read More

build.rs, link, and pkg-config

5 minute

In FFI, declaring the C function is only the first step:

unsafe extern "C" {
    fn sensor_read(out: *mut u8, len: usize) -> i32;
}

This only tells Rust that a C ABI function named sensor_read exists.

The linker still needs to know:

which library contains this function?
where is the library file?
what other dependent libraries are needed?
during cross compilation, are these target libraries or host libraries?

C projects usually handle this with Makefiles, CMake, pkg-config, sysroots, and cross toolchains. Rust projects use #[link], build.rs, and build dependencies for the same job.

Read More

Static Libraries, Dynamic Libraries, and ABI

6 minute

The previous FFI articles focused on function-level boundaries: extern "C", strings, memory ownership, callbacks, and void *ctx.

When delivering Rust to a C project, there is another layer:

What file does the C project link?
What symbols are declared in the header?
Can those symbols keep a stable ABI?
After Rust internals change, does C need to change?

C developers know .a, .so, .h, and ABI. Rust delivered to C should return to those concepts instead of exposing Cargo internals.

Read More

FFI Callbacks and void *ctx

5 minute

C callback APIs usually look like this:

typedef void (*event_cb_t)(void *ctx, int event);

int device_register_callback(struct device *dev, event_cb_t cb, void *ctx);
void device_unregister_callback(struct device *dev);

The function pointer is the code to run. ctx carries the caller’s environment.

Inside Rust, closures express “code plus environment.” But across a C ABI, a capturing closure cannot directly become a C function pointer. C understands a fixed-ABI function address and a void *ctx.

So FFI callbacks are mainly a lifetime and ownership problem:

Where is the callback object stored?
If C stores ctx, is the Rust owner still alive?
Which thread invokes the callback?
Can C call after unregister?
Can destruction race with a running callback?

C Needs an extern Function, Not a Capturing Closure

A Rust function that captures no environment can be used as a C callback:

Read More

C Strings, Memory Ownership, and Error Returns

7 minute

In FFI, extern "C" is only the entry point.

The most fragile parts are usually:

whether a C string is a valid NUL-terminated string
who allocates returned pointers, who frees them, and which function frees them
whether errors cross the boundary as integer codes, output parameters, or text

C code handles these through API documentation and discipline. Rust can put some rules into types, but the language boundary still needs an explicit contract.

Read More

Concurrency: Send, Sync, Arc, and Mutex

6 minute

In C, the basic threading tools are direct:

pthread_create(&tid, NULL, worker, ctx);
pthread_mutex_lock(&lock);
/* modify shared state */
pthread_mutex_unlock(&lock);

These tools can build reliable systems, but many rules are conventions:

  • whether ctx points to data that may cross threads
  • who owns that data
  • whether multiple threads read and write it at the same time
  • whether locking was forgotten
  • which fields are protected by which lock
  • whether resources outlive callbacks and thread exit paths

Rust does not make concurrency automatically simple. It moves some thread-safety requirements into types:

Read More

Closures and Callbacks

6 minute

C callbacks usually look like a function pointer plus a context pointer:

typedef void (*event_cb_t)(void *ctx, int event);

void register_event(event_cb_t cb, void *ctx);

cb is the function to call, and ctx is the environment passed back during the call. This pattern is mature, but the old problems remain:

  • the real type of ctx is a convention
  • the lifetime of ctx is a convention
  • whether the callback mutates the environment is a convention
  • whether the callback is called once or many times is a convention
  • thread-safety requirements are a convention

Rust closures solve the same problem: keep the code to run together with the environment it needs.

Read More

iterator

6 minute

C programmers usually traverse data with indexes or pointers:

for (size_t i = 0; i < len; i++) {
    sum += data[i];
}

Or:

for (uint8_t *p = buf; p < buf + len; p++) {
    *p ^= key;
}

Rust can write loops too, but the more common model is the iterator. It is not just fancy syntax. It turns traversal into a value that can be passed, composed, and consumed.

let sum: u32 = data.iter().map(|b| *b as u32).sum();

This article focuses on the engineering model C programmers need: how to move from index loops to iterators, when traversal borrows, when it mutably borrows, and when it consumes a container.

Read More