Demilade Sonuga's blog

All posts

Implementing Trait Objects II

2023-04-23

In this post, we're going to test our function trait objects.

What Exactly Are We Testing?

The requirements for our boxed functions:

  1. Function behaviour. If we have a boxed function f, we must be able to treat f as if it was a normal function.
  2. Ability to modify the environment, like a regular closure.
  3. Trait implementations.
  4. Ability to use them with vectors (because we're going to do that in our event handling scheme).

Dummy Allocators

Again, we need dummy allocators. For now, we're just going to copy and paste:

In boxed_fn.rs

#[cfg(test)]
mod tests {

    // Convenience function for getting the always fail allocator
    fn failing_allocator() -> *mut FailingAllocator {
        &mut FailingAllocator as *mut _
    }

    // Convenience function for getting the always successful allocator
    fn successful_allocator() -> *mut SuccessfulAllocator {
        &mut SuccessfulAllocator as *mut _
    }

    // Dummy allocator that we can depend on to always succeed
    struct SuccessfulAllocator;

    use std::alloc::Global as PlatformAllocator;
    use std::alloc::Layout;
    use std::ptr::NonNull;
    use std::alloc::Allocator as StdAllocator;

    // Use your computer's allocator to allocate and deallocate memory
    // Much more reliable than using our own custom allocator,
    // so we can depend on it succeeding (under normal circumstances)
    unsafe impl Allocator for SuccessfulAllocator {
        unsafe fn alloc(&mut self, size: usize, alignment: usize) -> Option<*mut u8> {
            let mem_layout = Layout::from_size_align(size, alignment).unwrap();
            let mem = PlatformAllocator.allocate(mem_layout).unwrap();
            let ptr = mem.as_ptr() as *mut u8;
            Some(ptr)
        }
        unsafe fn dealloc(&mut self, ptr: *mut u8, size_to_dealloc: usize) {
            let mem_layout = Layout::from_size_align(size_to_dealloc, 1).unwrap();
            PlatformAllocator.deallocate(NonNull::new(ptr).unwrap(), mem_layout);
        }
    }

    
    // Dummy allocator we can depend on to always fail
    struct FailingAllocator;

    unsafe impl Allocator for FailingAllocator {
        unsafe fn alloc(&mut self, size: usize, alignment: usize) -> Option<*mut u8> {
            None
        }
        unsafe fn dealloc(&mut self, ptr: *mut u8, size_to_dealloc: usize) {}
    }
}

And for the testing utility to see the tests:

In main.rs

mod boxed;
mod vec;
mod boxed_fn; // NEW
mod event_hook;

Testing

Let's start from closure behaviour. The test must demonstrate that a BoxedFn behaves like a function and can manipulate its environment.

#[cfg(test)]
mod tests {
    use super::*;
    use crate::event_hook::EventInfo;

    #[test]
    fn test_fn_call() {
        let mut was_called = false;
        let f: _ = BoxedFn::new(|_| {
            was_called = true;
        }, successful_allocator());
        assert!(!was_called);
        f(EventInfo::Timer);
        assert!(was_called);
    }

    // ...Others
}

This test creates a variable was_called and initializes it to false. The closure in BoxedFn, when called, sets was_called to true. So, if invoking the BoxedFn instance results in a change of the was_called variable, two things have been demonstrated: the ability of the BoxedFn to behave like a normal function and its ability to modify its environment.

Running the tests should result in a success. Remember to mess around with the tests and experiment.

Next, we're going to test the ability to use the BoxedFn with vectors.

#[cfg(test)]
mod tests {
    // ...Others
    use crate::vec;
    // ...Others

    #[test]
    fn test_vec_of_boxed_fn() {
        let mut no_of_fns_called = 0;
        let allocator = successful_allocator();
        let mut v: vec::Vec<BoxedFn> = vec::Vec::with_capacity(3, allocator);
        v.push(BoxedFn::new(|_| no_of_fns_called += 1, allocator));
        v.push(BoxedFn::new(|_| no_of_fns_called += 1, allocator));
        v.push(BoxedFn::new(|_| no_of_fns_called += 1, allocator));
        v.iter().for_each(|f| f(EventInfo::Timer));
        assert_eq!(no_of_fns_called, 3);
    }

    // ...Others
}

This test creates a vector of 3 BoxedFns each of which increases a single variable in the enviornment by 1 anytime the function is called. Calling all three functions will result in that variable ending up with the value 3. This test simply demonstrates that the BoxedFn still behaves the way it ought to behave even when it's in a vector.

To test the Clone implementation, we demonstrate that a cloned BoxedFn still does the exact same thing that the original BoxedFn would have done.

#[cfg(test)]
mod tests {
    // ...Others

    #[test]
    fn test_clone() {
        let allocator = successful_allocator();
        let mut x = 0;
        let f = BoxedFn::new(|_| x += 1, allocator);
        let g = f.clone();
        core::mem::drop(f);
        assert_eq!(x, 0);
        g(EventInfo::Timer);
        assert_eq!(x, 1);
    }

    // ...Others
}

This test first creates a BoxedFn, f. Then it creates a clone of f: g. It effectively demonstrates that the Clone implementation works because calling the clone g delivers the same effect as calling the original.

As for the Drop implementation, I'll leave you to decide how to test that.

Take Away

For the full code, go to the repo

In The Next Post

We get on with the event handling scheme