Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust FAQ: Function Pointers and Closures

PART22 -- Function Pointers and Closures

Q112: What is the type of a function pointer? Is it different from a closure?

Function Pointer Type: In Rust, a function pointer is a type that references a function with a specific signature. Its type is written as fn(Arg1, Arg2, ...) -> ReturnType. Function pointers are used to refer to named functions or anonymous functions with no captured variables.

  • Syntax: fn(T1, T2, ...) -> R, where T1, T2, etc., are argument types, and R is the return type.
  • Example:
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    fn main() {
        let func_ptr: fn(i32, i32) -> i32 = add; // Function pointer
        println!("{}", func_ptr(2, 3)); // Prints: 5
    }
  • Characteristics:
    • Fixed size (a single pointer to the function’s code).
    • No captured environment; points directly to a function.
    • Can be used in FFI (e.g., with C) due to its simple, C-compatible representation.

Closure Type: A closure in Rust is an anonymous function that can capture variables from its surrounding environment. Each closure has a unique, anonymous type generated by the compiler, implementing one or more of the closure traits: Fn, FnMut, or FnOnce.

  • Syntax: |arg1, arg2, ...| { body }.
  • Example:
    fn main() {
        let x = 10;
        let closure = |a: i32| a + x; // Captures x
        println!("{}", closure(5)); // Prints: 15
    }
  • Characteristics:
    • Unique type per closure, even if signatures match.
    • May capture variables, affecting its size and lifetime.
    • Implements Fn (immutable borrow), FnMut (mutable borrow), or FnOnce (consumes captured variables).

Differences:

  • Type: Function pointers have a uniform type (fn(...) -> ...), while each closure has a unique, anonymous type.
  • Captured Environment:
    • Function pointers: No environment; just a pointer to code.
    • Closures: Can capture variables, storing them in a hidden struct.
  • Flexibility:
    • Function pointers: Limited to named functions or closures with no captures.
    • Closures: More flexible, supporting environment capture and inlined logic.
  • FFI Compatibility:
    • Function pointers: Compatible with C due to fixed size and no environment.
    • Closures: Not directly C-compatible due to variable size and captures.
  • Example:
    #![allow(unused)]
    fn main() {
    let func_ptr: fn(i32) -> i32 = |x| x + 1; // Works: no captures
    let x = 10;
    // let func_ptr: fn(i32) -> i32 = |y| y + x; // Error: captures x
    let closure = |y| y + x; // Closure with unique type
    }

Best Practice:

  • Use function pointers for simple, C-compatible callbacks or when no captures are needed.
  • Use closures for flexible, context-aware logic.
  • Use trait bounds (Fn, FnMut, FnOnce) to abstract over closures and function pointers.

Q113: How can I ensure Rust structs are only created with Box?

To ensure a Rust struct is only created with Box, you can make the struct’s fields private and provide a factory function (e.g., new) that returns Box<Self>. This enforces heap allocation and encapsulates creation logic.

Steps:

  1. Make the Struct Private:
    • Define the struct in a module and make it private (no pub on the struct).
    • Use pub only for the factory function.
  2. Provide a Factory Function:
    • Create a pub fn new(...) -> Box<Self> that constructs the struct and wraps it in a Box.
  3. Restrict Direct Access:
    • Ensure fields are private to prevent manual construction outside the factory.

Example:

mod shapes {
    // Private struct
    struct Circle {
        radius: f64, // Private field
    }

    impl Circle {
        // Public factory function
        pub fn new(radius: f64) -> Box<Self> {
            if radius < 0.0 {
                panic!("Radius cannot be negative");
            }
            Box::new(Circle { radius })
        }

        pub fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    }
}

fn main() {
    let circle = shapes::Circle::new(5.0); // Returns Box<Circle>
    println!("Area: {}", circle.area()); // Prints: Area: 78.53981633974483
    // let c = shapes::Circle { radius: 5.0 }; // Error: Circle is private
}

Alternative: Sealed Trait: Use a sealed trait to enforce Box creation while allowing trait-based polymorphism:

mod shapes {
    pub trait Shape {
        fn area(&self) -> f64;
    }

    // Private struct
    struct Circle {
        radius: f64,
    }

    impl Shape for Circle {
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    }

    // Factory function returning trait object
    pub fn new_circle(radius: f64) -> Box<dyn Shape> {
        if radius < 0.0 {
            panic!("Radius cannot be negative");
        }
        Box::new(Circle { radius })
    }
}

fn main() {
    let circle = shapes::new_circle(5.0); // Returns Box<dyn Shape>
    println!("Area: {}", circle.area());
}

Key Points:

  • Encapsulation: Private structs prevent direct instantiation, forcing use of the factory.
  • Heap Allocation: Box ensures the struct is heap-allocated, useful for large data or trait objects.
  • Validation: Factory functions can enforce invariants (e.g., non-negative radius).
  • Trade-offs: Adds indirection overhead but ensures controlled creation.

Best Practice:

  • Use private structs with factory functions to enforce Box creation.
  • Consider Box<dyn Trait> for polymorphism if multiple types are involved.
  • Document why Box is required (e.g., large data, recursive types).

Q114: How do I pass a closure to a C callback or event handler?

Passing a Rust closure to a C callback or event handler is challenging because closures have unique, anonymous types and may capture an environment, which is not directly compatible with C’s function pointer-based callbacks. To achieve this, you need to use a function pointer or wrap the closure in a way that C can call it.

Steps:

  1. Understand C Callback Requirements:
    • C callbacks typically expect a function pointer (e.g., void (*callback)(void*)) and an optional void* user data pointer to pass context.
  2. Use a Function Pointer:
    • If the closure doesn’t capture variables, convert it to a fn pointer.
    • If it captures variables, use a Box to store the closure and pass a void* to the C callback.
  3. Wrap the Closure:
    • Store the closure in a Box and pass its raw pointer as user data.
    • Define a C-compatible wrapper function with extern "C" and #[no_mangle] to call the closure.
  4. Manage Lifetimes:
    • Ensure the closure’s lifetime outlives the C callback to avoid dangling pointers.
  5. Free Resources:
    • Manually free the Box when the callback is no longer needed to avoid memory leaks.

Example: Passing a Rust closure to a C function that expects a callback.

use std::os::raw::{c_int, c_void};

// C function that takes a callback and user data
extern "C" {
    fn register_callback(cb: extern "C" fn(c_int, *mut c_void), data: *mut c_void);
}

// Wrapper function to call the closure
extern "C" fn call_closure(x: c_int, data: *mut c_void) {
    unsafe {
        let closure = &mut *(data as *mut Box<dyn FnMut(i32)>);
        closure(x); // Call the closure
    }
}

fn main() {
    let mut counter = 0;
    let closure = Box::new(move |x: i32| {
        counter += x;
        println!("Counter: {}", counter);
    }) as Box<dyn FnMut(i32)>;

    // Leak the Box to ensure it lives long enough
    let closure_ptr = Box::into_raw(Box::new(closure)) as *mut c_void;

    unsafe {
        register_callback(call_closure, closure_ptr);
    }

    // Note: In a real application, call a C function to trigger the callback
}

C Code (example):

typedef void (*Callback)(int, void*);

void register_callback(Callback cb, void* data) {
    // Simulate calling the callback
    cb(5, data);
}

Key Points:

  • Closure Traits: Use FnMut for mutable closures, Fn for immutable, or FnOnce for one-shot closures (harder to use with C).
  • Box for Captures: Since closures with captures aren’t fn pointers, store them in a Box<dyn FnMut(...)> and pass the pointer to C.
  • Safety: Use unsafe for FFI calls and pointer conversions; ensure no dangling pointers.
  • Memory Management: Use Box::into_raw to pass ownership to C, and reclaim with Box::from_raw to free later.
  • Lifetime: Ensure the closure lives as long as the C callback might be called (e.g., leak the Box or manage its lifetime explicitly).

Best Practice:

  • Use Box<dyn FnMut(...)> for closures with captures.
  • Define a C-compatible wrapper function with extern "C".
  • Use cbindgen to generate C headers for Rust FFI functions.
  • Test thoroughly to avoid memory leaks or undefined behavior.

Q115: Why am I having trouble taking the address of a Rust function?

Trouble taking the address of a Rust function typically arises from Rust’s function pointer syntax, name mangling, or confusion with closures. Here are common issues and solutions:

  • Common Issues:

    1. Syntax Confusion:
      • Rust uses fn(...) -> ... for function pointers, and you must specify the exact signature.
      • Example:
        #![allow(unused)]
        fn main() {
        fn add(a: i32, b: i32) -> i32 { a + b }
        let ptr: fn(i32, i32) -> i32 = add; // Correct
        // let ptr = &add; // May work but less explicit
        }
      • Solution: Use the explicit fn type to avoid ambiguity.
    2. Name Mangling:
      • Rust mangles function names to include type information, making them inaccessible to C or other languages.
      • Example:
        #![allow(unused)]
        fn main() {
        pub fn my_function() {}
        // Mangled name in object file: _ZN10my_function...
        }
      • Solution: Use #[no_mangle] for C-compatible functions:
        #![allow(unused)]
        fn main() {
        #[no_mangle]
        pub extern "C" fn my_function() {}
        }
    3. Closures vs. Functions:
      • Closures have unique types and may capture variables, preventing direct conversion to a fn pointer unless they capture nothing.
      • Example:
        #![allow(unused)]
        fn main() {
        let x = 10;
        let closure = |a| a + x;
        // let ptr: fn(i32) -> i32 = closure; // Error: closure captures x
        let no_capture = |a| a + 1;
        let ptr: fn(i32) -> i32 = no_capture; // Works
        }
      • Solution: Ensure the function or closure captures no variables, or use Box<dyn Fn(...)> for closures (see Q114).
    4. FFI Context:
      • When passing to C, the function must use extern "C" to match the C ABI.
      • Example:
        #![allow(unused)]
        fn main() {
        extern "C" fn my_callback() {}
        let ptr: extern "C" fn() = my_callback; // Correct
        }
      • Solution: Add extern "C" and #[no_mangle] for FFI.
    5. Module Visibility:
      • If the function is in a private module, it’s inaccessible outside.
      • Solution: Make the function pub or adjust module visibility.
  • Debugging Tips:

    • Check the function signature matches the fn type exactly.
    • Use nm to inspect symbol names in the compiled binary:
      nm -g target/release/libmy_crate.a
      
    • Ensure extern "C" and #[no_mangle] for FFI contexts.
    • For closures, use Box<dyn Fn(...)> or refactor to a named function.

Best Practice:

  • Use explicit fn(...) -> ... types for function pointers.
  • Add #[no_mangle] and extern "C" for C interoperability.
  • Avoid closures for function pointers unless they capture no variables.
  • Test function pointers in isolation to catch type or linkage issues.

Q116: How do I declare an array of function pointers or closures?

Declaring an array of function pointers or closures in Rust requires different approaches due to their type differences. Function pointers have a uniform type, while closures have unique types, requiring trait objects or other workarounds for arrays.

Array of Function Pointers:

  • Function pointers have a consistent fn(...) -> ... type, making them suitable for arrays.
  • Syntax: [fn(Args) -> ReturnType; N] for an array of N function pointers.
  • Example:
    fn add(a: i32, b: i32) -> i32 { a + b }
    fn sub(a: i32, b: i32) -> i32 { a - b }
    
    fn main() {
        let funcs: [fn(i32, i32) -> i32; 2] = [add, sub];
        println!("{}", funcs[0](5, 3)); // Prints: 8
        println!("{}", funcs[1](5, 3)); // Prints: 2
    }

Array of Closures:

  • Closures have unique, anonymous types, so you cannot directly create an array of closures unless they’re coerced to function pointers (no captures) or wrapped in trait objects (Box<dyn Fn(...)>).
  • Options:
    1. Coerce to Function Pointers (if closures capture no variables):
      #![allow(unused)]
      fn main() {
      let c1 = |x: i32| x + 1;
      let c2 = |x: i32| x * 2;
      let funcs: [fn(i32) -> i32; 2] = [c1, c2]; // Works: no captures
      }
    2. Use Trait Objects:
      • Store closures as Box<dyn Fn(...)> to handle different types.
      • Example:
        fn main() {
            let x = 10;
            let c1 = Box::new(|a: i32| a + x) as Box<dyn Fn(i32) -> i32>;
            let c2 = Box::new(|a: i32| a * x) as Box<dyn Fn(i32) -> i32>;
            let funcs: [Box<dyn Fn(i32) -> i32>; 2] = [c1, c2];
            println!("{}", funcs[0](5)); // Prints: 15
            println!("{}", funcs[1](5)); // Prints: 50
        }
    3. Use a Vec for Dynamic Size:
      • If the number of closures isn’t fixed, use Vec<Box<dyn Fn(...)> >.
      • Example:
        #![allow(unused)]
        fn main() {
        let mut funcs: Vec<Box<dyn Fn(i32) -> i32>> = Vec::new();
        funcs.push(Box::new(|x| x + 1));
        funcs.push(Box::new(|x| x * 2));
        }

Key Points:

  • Function Pointers: Use [fn(...) -> ...; N] for fixed-size arrays of named functions or non-capturing closures.
  • Closures: Use [Box<dyn Fn(...)>; N] or Vec<Box<dyn Fn(...)> > for closures with captures, as each closure has a unique type.
  • Performance: Function pointers are lighter (no heap allocation, static dispatch), while Box<dyn Fn(...)> involves heap allocation and dynamic dispatch.
  • FFI: For C compatibility, use function pointers (fn), as closures are not C-compatible.

Best Practice:

  • Use function pointers for arrays when possible (e.g., no captures, C interop).
  • Use Box<dyn Fn(...)> for closures with captures or when polymorphism is needed.
  • Prefer Vec over arrays for dynamic sizes.
  • Document the expected signature and behavior of stored functions/closures.