Skip to main content trait: From Function Pointer Tables to Behavior Abstraction | IoT Worker

trait: From Function Pointer Tables to Behavior Abstraction

C has no language-level interface, but C projects have always had interfaces.

Driver tables, callback tables, vtables, opaque pointers, and void *ctx plus function pointers all express the same idea: an object can perform a group of operations.

Rust trait also expresses behavior. The difference is that Rust puts “this type must provide these methods” into the type system instead of relying only on struct fields and calling conventions.

This article covers the first use of traits: migrating C function pointer tables into Rust behavior abstraction.

Function Pointer Tables in C

Consider a simple output interface:

#include <stddef.h>
#include <stdint.h>

struct WriterOps {
    int (*write)(void *ctx, const uint8_t *buf, size_t len);
    int (*flush)(void *ctx);
};

struct Writer {
    void *ctx;
    const struct WriterOps *ops;
};

int writer_write(struct Writer *writer, const uint8_t *buf, size_t len)
{
    return writer->ops->write(writer->ctx, buf, len);
}

This pattern is common. ctx points to the concrete object, and ops provides the operation table. Different implementations fill different function pointers and reuse the same calling code.

The familiar problems are:

  • the real type behind ctx is convention
  • whether ops is null is convention
  • whether each function pointer exists is convention
  • the lifetime and release responsibility of ctx are convention
  • signature mismatch may only show up at runtime

C can make this stable, but it requires discipline.

trait Expresses a Set of Behaviors

Rust can write:

trait Writer {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<()>;
    fn flush(&mut self) -> std::io::Result<()>;
}

This means: any type implementing Writer must provide write and flush.

Example:

struct MemoryWriter {
    data: Vec<u8>,
}

impl Writer for MemoryWriter {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<()> {
        self.data.extend_from_slice(buf);
        Ok(())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

impl Writer for MemoryWriter means MemoryWriter provides the behavior required by Writer.

This is similar to a C ops table, but method signatures, parameter types, and return values are checked by the compiler.

trait Is Not Data Layout

C interface tables have explicit memory layout:

struct WriterOps {
    int (*write)(...);
    int (*flush)(...);
};

A Rust trait itself is not a C ABI struct and should not be treated as a layout to pass across FFI.

A trait is a behavior constraint in Rust’s type system. It answers:

can this type call these methods?
what behavior does this function require from its parameter?

For C interop, use extern "C", explicit layouts, and function pointers at the FFI boundary. Traits can wrap that boundary inside Rust.

Trait Bounds on Generic Functions

If a function only cares that something can write, not what concrete type it is, write:

fn write_header<W: Writer>(writer: &mut W) -> std::io::Result<()> {
    writer.write(&[0x12, 0x34])?;
    writer.flush()
}

W: Writer means W may be any type implementing Writer.

Usage:

let mut writer = MemoryWriter { data: Vec::new() };
write_header(&mut writer)?;

This is similar in goal to passing struct Writer * in C. The caller provides the concrete implementation, and common code depends only on the interface.

The difference is that Rust knows W implements Writer and knows the exact signatures of write and flush.

dyn trait Means Runtime Dispatch

Sometimes you want runtime interface dispatch, similar to a C function pointer table.

Use a trait object:

fn write_footer(writer: &mut dyn Writer) -> std::io::Result<()> {
    writer.write(&[0xff])?;
    writer.flush()
}

You can first read &mut dyn Writer as:

pointer to some object implementing Writer + method table

That is close to C ctx + ops.

The first distinction is:

W: Writer        concrete type known at compile time, static dispatch
&mut dyn Writer  runtime method table, dynamic dispatch

Generics will be covered later. For now: traits can serve both compile-time abstraction and runtime interface objects.

Traits Can Provide Default Methods

In a C ops table, an optional function often means checking a null pointer:

if (writer->ops->flush != NULL) {
    writer->ops->flush(writer->ctx);
}

Rust traits can provide default method bodies:

trait Writer {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<()>;

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

Implementors must provide write, but may use the default flush.

This is clearer than “the function pointer may be NULL.” The default behavior lives in the trait definition instead of being hidden in caller-side checks.

Traits Make Library APIs Clearer

If you write an encoding library, you may not want to force output into a Vec<u8> or a file. Let the caller provide a writer:

fn encode_message<W: Writer>(writer: &mut W, payload: &[u8]) -> std::io::Result<()> {
    writer.write(&[payload.len() as u8])?;
    writer.write(payload)?;
    writer.flush()
}

The same logic can write to memory, a file, a socket, or a test mock.

C can do this with ops tables. Rust traits make the interface constraint, method signature, error type, and borrowing relationship visible in the type system.

What to Keep from This Article

The first mapping is:

function pointer          -> trait method
ops table / vtable        -> trait / dyn trait
void *ctx                 -> fields in the concrete implementation type
struct Interface *        -> &mut dyn Trait
require a set of methods  -> T: Trait
optional function pointer -> default trait method

A trait is not a C struct layout and not an FFI interface by itself. It is Rust’s way to express behavior constraints.

If the C question is “which operations should this object support?”, the Rust answer is often a trait.

The next article can cover generics. Traits and generics often appear together: generics answer “which types can this code work with,” and traits answer “what behavior must those types provide.”