Demilade Sonuga's blog
All postsImplementing Trait Objects I
This is the last thing we need to complete before we can fully get on with event handling (I promise this time). In this post, we're going to be implementing boxed trait objects for functions.
The Idea
The main idea behind the implementation I'm presenting here is that, in the C programming language, a pointer to a struct cast as a pointer to something else is guaranteed to keep pointing to the same address. To understand this, consider the following Rust-like pseudocode:
struct Base {
drop(pointer to self);
clone(pointer to self);
}
struct Child {
Base base;
some_other_func();
}
In the above, Base
is a structure of two function pointers: drop
and clone
. While Child
is a struct with
the first field as a Base
instance and the second field as another function pointer.
According to C's guarantees, casting a pointer to Child
as a pointer to Base
is valid because the first field
in Child
is a valid Base
.
For example, suppose you had a pointer to a Child
instance:
let child_ptr = &child as *mut Child;
When you do this, the compiler's view of child
's memory will look something like this:
Memory:
0: base - drop
1: - clone
2: some_other_func
A Base
instance is at address 0 and together with the some_other_func
function pointer is a Child
instance. child_ptr
will have the value 0 since that is the address of the first byte.
Casting this pointer as a pointer to Base
, like so:
let base_ptr = child_ptr as *mut Base;
just tells the compiler: "Yo! Interpret the value starting at this address as a Base
, not a Child
anymore.
Alright?"
The compiler then goes: "Okay, since only the bytes in addresses 0 and 1 make up the Base
, I'll keep looking
at that and forget about address 2 (and the other addresses that come after it)"
The compiler's view then becomes:
Memory:
0: base - drop
1: - clone
Like the some_other_func
never existed. It just becomes a regular Base
. The other really wonderful thing
about it all is that (if we remember correctly), base_ptr
was once a pointer to a Child
, and the
some_other_func
never got overwritten. So, we can still cast the pointer base_ptr
back to a Child
pointer.
This is all just a very simple implementation of polymorphism. If you're interested in finding out more about this idea, check the references.
Aims
Our function trait object should be easily created like so:
let boxed_fn = BoxedFn::new(a_function);
And called like so:
boxed_fn(...arguments required);
Implementation
In this implementation, we're going to start from a structure called BaseFn
. You can think of BaseFn
as
the Base
struct in the previous section. It specifies functions that all our function trait objects must have:
Create a new file, boxed_fn.rs
, and type this in:
use crate::event_hook::EventInfo;
// The polymorphic representation of a base function which can stand in
// for any event handler function
#[repr(C)]
struct BaseFn {
call_boxed: fn(pointer to data, EventInfo) -> (),
drop: fn(pointer to data) -> (),
clone: fn(pointer to data) -> BoxedFn
}
In main.rs
mod vec;
mod boxed_fn;
mod event_hook; // NEW
mod allocator;
In event_hook.rs
use crate::keyboard::KeyEvent; // NEW
/* COMMENTING THIS OUT, FOR NOW TO AVOID FUTURE ERRORS
// Mediator between the game code and the interrupt service routines
pub struct EventHooker {
// The functions that will be called when an event occurs
handlers: Vec<Box<dyn FnMut(EventInfo)>>
}
*/
// The info about an event that will be passed into a handler
// when it is called
pub enum EventInfo {
Timer,
Keyboard(KeyEvent)
}
Remember from the previous post that a trait object is a structure holding a pointer to a method table and
a pointer to data of a specific type that implements that trait. This BaseFn
is our trait object's method
table.
The call_boxed
function is the method that will serve to call the actual functions themselves like boxed_fn()
.
The drop
function is the method that will serve as the destructor. And the clone
serves to, well, clone.
call_boxed
takes an EventInfo
because, earlier, we imposed the restriction that all event handler functions
must take a single argument of the type EventInfo
.
Instances of this BaseFn
are going to have pointers to different implementations of drop
and clone
and call_boxed
, for each separate function trait object.
Next, we'll create the trait object itself:
use crate::allocator::Allocator;
// A stand in for `Box<dyn FnMut>`
pub struct BoxedFn(*mut BaseFn, *mut dyn Allocator);
This is the data that gets passed to the BaseFn
functions:
#[repr(C)]
struct BaseFn {
call_boxed: fn(*const BoxedFn, EventInfo) -> (),
drop: fn(*const BoxedFn) -> (),
clone: fn(*const BoxedFn) -> BoxedFn
}
This struct will serve as our trait object. If you look very closely, you'll notice that this doesn't fully fit our picture of trait objects since it was mentioned that they are pointers to a method table and data. This struct only contains the pointer to the method table, but not the data.
This is really not a problem. It's just an implementation choice and you can do it another way if you want to.
Instead of keeping another pointer to the function/function-like thing (stuff like closures), we do this:
#[repr(C)]
struct Repr<F: FnMut(EventInfo)> {
base: BaseFn,
func: F
}
By annotating this with #[repr(C)]
, we tell Rust to behave like a C compiler when dealing with this struct.
This enables us to get a pointer to BaseFn
from a Repr
by simply casting a *mut Repr
to *mut BaseFn
and
forgetting about the func
field. We add the trait bound F: FnMut(EventInfo)
to make the compiler ensure that
func
is a function-like thing that can be called and takes an EventInfo
as an argument.
Creating a new BoxedFn
is now just a matter of doing the following:
- Accepting the function or function-like thing that we want boxed.
- Creating
call_boxed
,drop
, andclone
functions for the thing we want boxed and putting them together into aBaseFn
instance. - Creating a
Repr
from the newBaseFn
and the function-like thing. - Putting the
Repr
on the heap, so that it will outlive the function. - Retrieving the pointer to the
Repr
and casting it to a*mut BaseFn
. - Use that
*mut BaseFn
to create the newBoxedFn
.
The only step in this list that doesn't seem easy to resolve is number 2. How do we create call_boxed
, drop
,
and clone
functions for each of those things we want boxed? What if there are a hundred of them? Do we
then start writing one hundred implementations each? No. That is so unprogrammerly.
You have to think: what mechanism does Rust offer to us that allows us to automatically create different versions of a function based on their different types? And how do we make use of it? Make sure you answer these questions before reading on.
Generics, of course. Remember that the Rust compiler monomorphizes generic functions, that is it creates multiple copies of the function for each type that takes the place of its generic.
We can use this to our advantage. Let's start with call_boxed
. The role of this function is to call the boxed
function thing.
// Calls the concrete function wrapped by the BoxedFn
fn call_boxed<F>(boxed_fn_ptr: *const BoxedFn, event: EventInfo) where F: FnMut(EventInfo) {
unsafe {
let concrete_repr_ptr = (*boxed_fn_ptr).0 as *mut Repr<F>;
((*concrete_repr_ptr).func)(event)
}
}
This code is very straightforward. First, it casts the BoxedFn
's *mut BaseFn
pointer as a Repr
(We can do
this because a *mut BaseFn
in a BoxedFn
points to the first field of a Repr
).
Then it simply calls the function thing.
For the drop
implementation, all we need to do is deallocate the memory being used by the Repr
:
use crate::boxed::Box;
// Drops the boxed function
fn drop<F>(boxed_fn_ptr: *const BoxedFn) where F: FnMut(EventInfo) {
unsafe {
let base_fn_ptr = (*boxed_fn_ptr).0;
let concrete_ptr: *mut Repr<F> = base_fn_ptr as *mut Repr<F>;
let allocator = (*boxed_fn_ptr).1;
Box::<Repr<F>>::from_raw(concrete_ptr, allocator);
// Box is dropped at the end of the scope
}
}
The above simply casts the BoxedFn
's BaseFn
pointer as a *mut Repr
, constructs Box
from it and
allows the Box
to get dropped. The Box
's Drop
implementation then handles the memory deallocation.
The purpose of clone
is to create a new identical BoxedFn
:
// Clones the boxed function
//
// # Safety
//
// Cloning a BoxedFn is highly unsafe. The function may contains mutable
// references to the outer scope. Cloning the BoxedFn will result in cloning
// mutable references, defeating Rust's safety guarantees.
fn clone<F>(boxed_fn_ptr: *const BoxedFn) -> BoxedFn where F: FnMut(EventInfo) {
unsafe {
let base_fn_ptr = (*boxed_fn_ptr).0;
let concrete_ptr: *mut Repr<F> = base_fn_ptr.cast::<Repr<F>>();
let allocator = (*boxed_fn_ptr).1;
let func_ptr = &(*concrete_ptr).func as *const F;
BoxedFn::new(func_ptr.read(), allocator)
}
}
Again, the pointer casting stuff, but the function thing gets copied out with read
and a new BoxedFn
is
created from the copy. This function has some serious safety problems that just may screw us up in the future,
so we'll have to be careful with it.
Now, with these, let's create BoxedFn::new
.
impl BoxedFn {
// Creates a new BoxedFn from the given function-thing
pub fn new<F>(func: F, allocator: *mut dyn Allocator) -> Self where F: FnMut(EventInfo) {
}
}
Steps 1 and 2, putting the function thing's BaseFn
functions together into a BaseFn
instance:
impl BoxedFn {
// Creates a new BoxedFn from the given function-thing
pub fn new<F>(func: F, allocator: *mut dyn Allocator) -> Self where F: FnMut(EventInfo) {
let base_fn = BaseFn { call_boxed: call_boxed::<F>, drop: drop::<F>, clone: clone::<F> };
}
}
Notice that new
is a generic function. Because of this, call_boxed
, drop
, and clone
will have separate
copies for each type that is passed as the func
argument to BoxedFn::new
.
Step 3, creating a Repr
from the new BaseFn` and the function-thing:
impl BoxedFn {
// Creates a new BoxedFn from the given function-thing
pub fn new<F>(func: F, allocator: *mut dyn Allocator) -> Self where F: FnMut(EventInfo) {
let base_fn = BaseFn { call_boxed: call_boxed::<F>, drop: drop::<F>, clone: clone::<F> };
// NEW:
let concrete_repr = Repr {
base: base_fn,
func
};
}
}
Steps 4, 5, and 6: putting the Repr
on the heap, retrieving the pointer and casting it to *mut BaseFn
,
then creating a new BoxedFn
from that *mut BaseFn
.
impl BoxedFn {
// Creates a new BoxedFn from the given function-thing
pub fn new<F>(func: F, allocator: *mut dyn Allocator) -> Self where F: FnMut(EventInfo) {
let base_fn = BaseFn { call_boxed: call_boxed::<F>, drop: drop::<F>, clone: clone::<F> };
let concrete_repr = Repr {
base: base_fn,
func
};
// NEW:
let concrete_repr_ptr: *mut Repr<F> = Box::<Repr<F>>::into_raw(Box::new(concrete_repr, allocator));
let polymorphic_ptr: *mut BaseFn = concrete_repr_ptr as *mut BaseFn;
BoxedFn(polymorphic_ptr, allocator)
}
}
Notice that we're using the Box
's into_raw
function. This function first calls ManuallyDrop
on the
Box
, so that the Box
won't get dropped at the end of the function.
And that's it for creating a new BoxedFn
.
As for Drop
and Clone
implementations:
impl Drop for BoxedFn {
fn drop(&mut self) {
let base_fn_ptr = self.0;
unsafe { ((*base_fn_ptr).drop)(self as *const BoxedFn) };
}
}
impl Clone for BoxedFn {
fn clone(&self) -> Self {
let base_fn_ptr = self.0;
unsafe { ((*base_fn_ptr).clone)(self as *const BoxedFn) }
}
}
We make use of the implementations in the BaseFn
, which serve as the method table.
Finally, we need to make BoxedFn
callable, so we can do something like this: boxed_fn(event)
.
To do this, we need to implement FnOnce
, Fn
, and FnMut
, the traits that indicate that a structure is
callable. If you need a refresher on these traits, check this
post.
The thing about these function traits is that they're unstable, so we need to add modify main.rs
:
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]
#![feature(abi_efiapi, abi_x86_interrupt)]
#![feature(panic_info_message)]
#![feature(unboxed_closures, fn_traits)] // NEW
#![cfg_attr(test, feature(allocator_api))]
First, we'll implement Fn
:
impl Fn<(EventInfo,)> for BoxedFn {
extern "rust-call" fn call(&self, args: (EventInfo,)) -> Self::Output {
let base_fn_ptr = self.0;
unsafe { ((*base_fn_ptr).call_boxed)(self as *const BoxedFn, args.0) }
}
}
This implementation simply calls the call_boxed
function in the method table (in the BaseFn
instance).
The remaining implementations will just call this call
function.
impl FnMut<(EventInfo,)> for BoxedFn {
extern "rust-call" fn call_mut(&mut self, args: (EventInfo,)) -> Self::Output {
self.call(args)
}
}
impl FnOnce<(EventInfo,)> for BoxedFn {
type Output = ();
extern "rust-call" fn call_once(self, args: (EventInfo,)) -> Self::Output {
self.call(args)
}
}
And that's it for our implementation.
Take Away
For the full code, go to the repo
In The Next Post
We'll be testing our boxed functions.
References
- Polymorphism: https://adventures.michaelfbryan.com/posts/ffi-safe-polymorphism-in-rust/