Exposing crate functions to users

So I'm porting a simple game engine written in C++ to rust, and got it to a working level. Unfortunately I can't figure out how to expose a method that is called every loop of the engine. The method is meant to be implemented by the user of the crate. In the C++ code I'm porting, it's a virtual method. I've researched enough to know that inheritance isn't really favored in rust, but still wish to try and follow the code I'm porting as closely as possible. I wish to see what it looks like to force an inheritance approach and compare it with an approach more fitting to rust for knowledge sake.

I believe I read trait objects are what can give me virtual method-like functionality, but I can't seem to wrap my head around how to actually use traits in this manner. If anybody could give me an example of how to expose a method to a crate user, that would be very much appreciated. If someone would be willing to look at my code and give me a more targeted example of how they would do it, that would be awesome too.

My rust port: https://github.com/MarkAnthonyM/Olc-Console-Game-Engine/tree/dev (on_user_update() is the method I'm trying to expose)

Code I'm trying to port: https://github.com/OneLoneCoder/videos/blob/master/olcConsoleGameEngine.h

You probably don't want to create your own trait here, but rather store a closure that implements the existing FnMut trait. You would indeed be using trait objects in storing this closure, but all that is required is to store the closure in a Box, then pass a Box<dyn FnMut(&mut OlcConsoleGameEngine, Duration) -> bool into OlcConsoleGameEngine::new and store it in OlcConsoleGameEngine. You can then call it using the normal function call syntax and, since it is a closure, it can have additional data that it uses internally.

Edit: Okay, I just realised that this would create some lifetime issues if you're passing a reference to OlcConsoleGameEngine into the closure. The issue is that you could move or edit the closure while still calling it. This could be solved by wrapping the closure in an Option and calling Option::take to move it out of the struct before calling it, then returning it to the struct afterwards. That ensures the closure isn't inside the struct when being called.

1 Like

So I've been trying for the last 3-4 days following this advice and I've pushed further along, but have hit another wall. Don't know if its the lifetime issue @jameseb7 mentioned, although the compiler is mentioning Cannot borrow '*self' as mutable more than once at a time. I tried using an Option but couldn't get it to work. I think it's an ownership issue. Could anybody take a look at my code, and give me a pointer? About ready to toss my PC over here...What I tried is as follows:

Added field to OlcConsoleGameEngine struct:
update_function: Box<dyn FnMut(&mut OlcConsoleGameEngine)>

Added parameter to OlcConsoleGameEngine constructor function:
pub fn new(closure: Box<dyn FnMut(&mut OlcConsoleGameEngine)>

How closure gets ran: (is ran in engine loop over and over)

fn on_user_update(&mut self) {
    (self.update_function)(self);
}

When struct field and constructor parameter are defined without &mut OlcConsoleGameEngine, like so: Box<dyn FnMut()>, the code compiles and runs just fine. Using the library crate, I can toss in a closure as an argument to the constructor function and do stuff like print messages. I see them print out over and over as the engine loops. When I toss in &mut OlcConsoleGameEngine is when I get into trouble. Honestly, the idea of using OlcConsoleGameEngine as a parameter in the same function that is meant to construct it does weird things to my head. Any adive?

For a more complete look at the code
pub struct OlcConsoleGameEngine {
    app_name: String,

    console_handle: HANDLE,
    console_handle_in: HANDLE,

    enable_sound: bool,

    game_state_active: bool,

    mouse_pos_x: u32,
    mouse_pos_y: u32,

    rect_window: SMALL_RECT,

    screen_width: i16,
    screen_height: i16,

    text_buffer: Vec<CHAR_INFO>,

    update_function: Box<dyn FnMut(&mut OlcConsoleGameEngine)>,
}

impl OlcConsoleGameEngine {
    pub fn new(closure: Box<dyn FnMut(&mut OlcConsoleGameEngine)>) -> OlcConsoleGameEngine {
        let application_name = "default";
        let game_state_active = true;
        let mouse_x = 0;
        let mouse_y = 0;
        let output_handle = unsafe{ GetStdHandle(STD_OUTPUT_HANDLE) };
        let input_handle = unsafe{ GetStdHandle(STD_INPUT_HANDLE) };
        let rect_window = SMALL_RECT::empty();
        let window_buffer: Vec<CHAR_INFO> = Vec::new();

        OlcConsoleGameEngine {
            app_name: application_name.to_string(),
            console_handle: output_handle,
            console_handle_in: input_handle,
            enable_sound: true,
            game_state_active: game_state_active,
            mouse_pos_x: mouse_x,
            mouse_pos_y: mouse_y,
            rect_window: rect_window,
            screen_width: 80,
            screen_height: 80,
            text_buffer: window_buffer,
            update_function: closure,
        }
    }

    pub fn consturct_console(&mut self, width: i16, height: i16, font_w: i16, font_h: i16) {
        // Check for valid handle
        if self.console_handle == INVALID_HANDLE_VALUE {
            println!("failed to get valid console handle");
            return
        }

        self.screen_width = width;
        self.screen_height = height;

        //Set initial rect_window field
        self.rect_window = SMALL_RECT {
            Left: 0,
            Top: 0,
            Right: 1,
            Bottom: 1,
        };

        // Set window info using winapi
        self.set_console_window_info(self.console_handle, TRUE, &self.rect_window).unwrap();

        let coord = COORD {
            X: self.screen_width,
            Y: self.screen_height,
        };

        // Set the size of screen buffer
        self.set_console_screen_buffer_size(self.console_handle, coord).unwrap();

        // Assign screen buffer to console
        self.set_console_active_screen_buffer(self.console_handle).unwrap();

        // set font size and setting data
        let mut font_cfi = CONSOLE_FONT_INFOEX::empty();
        font_cfi.cbSize = size_of::<CONSOLE_FONT_INFOEX>().try_into().unwrap();
        font_cfi.nFont = 0;
        font_cfi.dwFontSize.X = font_w;
        font_cfi.dwFontSize.Y = font_h;
        font_cfi.FontFamily = FF_DONTCARE;
        font_cfi.FontWeight = FW_NORMAL.try_into().unwrap();

        // Set FaceName field for CONSOLE_FONT_INFOEX struct
        let face_name = format!("Consolas");
        let face_str = U16CString::from_str(face_name).unwrap();
        let face_ptr = face_str.as_ptr();
        let face_field_ptr = font_cfi.FaceName.as_mut_ptr();

        self.set_face_name(face_field_ptr, face_ptr);

        // Set extended information about current console font
        self.set_current_console_font_ex(self.console_handle, FALSE, &mut font_cfi).unwrap();

        // Initialize CONSOLE_SCREEN_BUFFER_INFO struct
        let mut screen_buffer_csbi = CONSOLE_SCREEN_BUFFER_INFO::empty();

        // Retrive information about supplied console handle
        self.get_console_screen_buffer_info(self.console_handle, &mut screen_buffer_csbi).unwrap();

        // Check for valid window size
        self.validate_window_size(&screen_buffer_csbi).unwrap();

        // Set physical console window size
        self.rect_window = SMALL_RECT {
            Left: 0,
            Top: 0,
            Right: self.screen_width - 1,
            Bottom: self.screen_height - 1,
        };

        self.set_console_window_info(self.console_handle, TRUE, &self.rect_window).unwrap();

        // Todo: Implement flag logic for mouse imput
        // self.set_console_mode().unwrap();

        // Initialize text buffer and allocate memory
        self.text_buffer = vec![CHAR_INFO::empty(); (self.screen_width * self.screen_height).try_into().unwrap()];

        // Todo: Implement logic to handle Ctrl+C functionality
        // self.set_console_ctrl_handler(handler_routine, bool);
    }

    //Todo: Implement sound
    // fn _enable_sound() {
    //
    // }

    fn get_console_screen_buffer_info(&self, console_handle: HANDLE, buffer_struct: PCONSOLE_SCREEN_BUFFER_INFO) -> Result<i32, &'static str> {
        let screen_buffer_info = unsafe { GetConsoleScreenBufferInfo(console_handle, buffer_struct) };

        if screen_buffer_info != 0 {
            return Ok(screen_buffer_info)
        } else {
            return Err("Get console active screen buffer function failed")
        }
    }

    fn set_console_active_screen_buffer(&self, console_handle: HANDLE) -> Result<i32, &'static str> {
        let active_buffer = unsafe { SetConsoleActiveScreenBuffer(console_handle) };

        if active_buffer != 0 {
            return Ok(active_buffer)
        } else {
            return Err("Set console active screen buffer function failed")
        }
    }

    fn set_console_screen_buffer_size(&self, console_handle: HANDLE, size: COORD) -> Result<i32, &'static str> {
        let set_size = unsafe { SetConsoleScreenBufferSize(console_handle, size) };

        if set_size != 0 {
            return Ok(set_size)
        } else {
            return Err("Set console screen buffer size function failed")
        }
    }

    fn set_console_title(&self, console_title: LPCWSTR) -> Result<i32, &'static str> {
        let title_string = unsafe { SetConsoleTitleW(console_title) };

        if title_string != 0 {
            return Ok(title_string)
        } else {
            return Err("Set console title function failed")
        }
    }

    fn set_console_window_info(&self, console_handle: HANDLE, absolute: BOOL, rect_struct: *const SMALL_RECT) -> Result<i32, &'static str> {
        let window_info = unsafe { SetConsoleWindowInfo(console_handle, absolute, rect_struct) };

        if window_info != 0 {
            return Ok(window_info)
        } else {
            return Err("Set console window info function failed")
        }
    }

    fn set_current_console_font_ex(&self, console_handle: HANDLE, max_window: BOOL, font_struct: PCONSOLE_FONT_INFOEX) -> Result<i32, &'static str> {
        let set_font = unsafe { SetCurrentConsoleFontEx(console_handle, max_window, font_struct) };

        if set_font != 0 {
            return Ok(set_font)
        } else {
            return Err("Set current console font function failed")
        }
    }

    //not sure this is right
    fn set_face_name(&self, string_1: LPWSTR, string_2: LPCWSTR) {
        unsafe { lstrcpyW(string_1, string_2) };
    }

    fn validate_window_size(&self, buffer_struct: &CONSOLE_SCREEN_BUFFER_INFO) -> Result<&'static str, &'static str> {
        if self.screen_height > buffer_struct.dwMaximumWindowSize.Y {
            return Err("Screen height or Font height is too big")
        } else if self.screen_width > buffer_struct.dwMaximumWindowSize.X {
            return Err("Screen width or Font Width is too big")
        } else {
            Ok("Window size validation successful")
        }
    }

    fn write_console_output(&self, console_handle: HANDLE, buffer: *const CHAR_INFO, buffer_size: COORD, buffer_coord: COORD, write_region: PSMALL_RECT) -> Result<i32, &'static str> {
        let buffer_output = unsafe { WriteConsoleOutputW(console_handle, buffer, buffer_size, buffer_coord, write_region) };

        if buffer_output != 0 {
            return Ok(buffer_output)
        } else {
            return Err("Write console output function failed")
        }
    }

    fn clip(&self, x: &mut i16, y: &mut i16) {
        // Todo: Replace with pattern match expression
        if *x < 0 { *x = 0; }
        if *x >= self.screen_width { *x = self.screen_width; }
        if *y < 0 { *y = 0; }
        if *y >= self.screen_height { *y = self.screen_height; }
    }

    fn draw(&mut self, x: usize, y: usize, c: SHORT, col: SHORT) {
        if x >= 0 && x < self.screen_width.try_into().unwrap() && y >= 0 && y < self.screen_height.try_into().unwrap() {
            unsafe {
                let mut chr: CHAR_INFO_Char = CHAR_INFO_Char::empty();
                *chr.UnicodeChar_mut() = c.try_into().unwrap();

                self.text_buffer[y * self.screen_width as usize + x].Char = chr;
                self.text_buffer[y * self.screen_width as usize + x].Attributes = col.try_into().unwrap();
            }
        }
    }

    // Todo: Test this function. Arithmetic may be wrong
    fn draw_circle(&mut self, xc: usize, yc: usize, r: usize, c: SHORT, col: SHORT) {
        let mut x = 0;
        let mut y = r;
        let mut p = 3 - 2 * r;
        if r == 0 {
            return
        }

        while y >= x { // only formulate 1/8 of circle
            self.draw(xc - x, yc - y, c, col);// upper left left
            self.draw(xc - y, yc - x, c, col);// upper upper left
            self.draw(xc + y, yc - x, c, col);// uper upper right
            self.draw(xc + x, yc - y, c, col);// upper right right
            self.draw(xc - x, yc + y, c, col);// lower left left
            self.draw(xc - y, yc + x, c, col);// lower lower left
            self.draw(xc + y, yc + x, c, col);// lower lower right
            self.draw(xc + x, yc + y, c, col);// lower right right
            if p < 0 {
                x += 1 + 6;
                p += 4 * x;
            } else {
                x += 1;
                y -= 1;
                p += 4 * (x - y) + 10;
            }
        }
    }

    // Todo: Test this function
    fn draw_line(&mut self, x_1: i16, y_1: i16, x_2: i16, y_2: i16, c: SHORT, col: SHORT) {
        let (mut x, mut y, xe, ye): (i16, i16, i16, i16);
        let dx = x_2 - x_1;
        let dy = y_2 - y_1;
        let dx_1 = dx.abs();
        let dy_1 = dy.abs();
        let mut px = 2 * dy_1 - dx_1;
        let mut py = 2 * dx_1 - dy_1;

        if dy_1 <= dx_1 {
            if dx >= 0 {
                x = x_1;
                y = y_1;
                xe = x_2;
            } else {
                x = x_2;
                y = y_2;
                xe = x_1;
            }

            self.draw(x as usize, y as usize, c, col);

            for _i in x .. xe {
                x += 1;

                if px < 0 {
                    px += 2 * dy_1;
                } else {
                    if (dx < 0 && dy < 0) || (dx > 0 && dy > 0) {
                        y += 1;
                    } else {
                        y -= 1;
                    }

                    px += 2 * (dy_1 - dx_1);
                }

                self.draw(x as usize, y as usize, c, col);
            }
        } else {
            if dy >= 0 {
                x = x_1;
                y = y_1;
                ye = y_2;
            } else {
                x = x_2;
                y = y_2;
                ye = y_1;
            }

            self.draw(x as usize, y as usize, c, col);

            for _i in y .. ye {
                y += 1;

                if py <= 0 {
                    py += 2 * dx_1;
                } else {
                    if (dx < 0 && dy < 0) || (dx > 0 && dy> 0) {
                        x += 1;
                    } else {
                        x -= 1;
                    }

                    py += 2 * (dx_1 - dy_1);
                }

                self.draw(x as usize, y as usize, c, col);
            }
        }
    }

    pub fn game_thread(mut self) {
        // Validate successful on_user_create function call
        self.on_user_create();

        // Todo: Implement sound system enable check

        // Window Title buffer
        let mut s: [wchar_t; 256] = [0; 256];
        let s_ptr = s.as_mut_ptr();

        // Window title information
        let mut w_char = format!("OneLoneCoder.com - Console Game Engine - {}", self.app_name);
        let mut w_string = U16CString::from_str(w_char).unwrap();
        let mut w_ptr = w_string.as_ptr();

        // Time deltas for smooth fps
        let mut tp_1 = Instant::now();
        let mut tp_2 = Instant::now();

        // Main game loop
        while self.game_state_active {
            while self.game_state_active {
                // Time delta calulations for smooth frame speed
                tp_2 = Instant::now();
                let mut elapsed_time = tp_2.duration_since(tp_1);
                let in_nano = elapsed_time.as_micros() as f64 / 100_000.0;
                tp_1 = tp_2;

                // Todo: Implement input handle logic

                // Todo: Implement user update function
                self.on_user_update();

                // Update title and push frame to buffer
                unsafe {
                    let mut rect = self.rect_window;
                    let rect_ptr = &mut rect;

                    w_char = format!("OneLoneCoder.com - Console Game Engine - {} - FPS: {:.2}", self.app_name, in_nano);
                    w_string = U16CString::from_str(w_char).unwrap();
                    w_ptr = w_string.as_ptr();

                    wsprintfW(s_ptr, w_ptr);

                    self.set_console_title(s.as_ptr()).unwrap();

                    self.write_console_output(self.console_handle, self.text_buffer.as_ptr(), COORD {X: self.screen_width, Y: self.screen_height}, COORD { X:0, Y:0 }, rect_ptr).unwrap();
                }
            }
        }

        // Todo: Implement free resources functions
    }

    fn on_user_create(&self) -> bool {
        true
    }

    fn on_user_update(&mut self) {
        (self.update_function)(self);
    }

    // fn on_user_update(&mut self, time_delta: Duration) -> bool {
    //     for x in 0..self.screen_width as usize {
    //         for y in 0..self.screen_height as usize {
    //             let ran_num = random::<u16>();
    //             let conv = ran_num % 16;
    //             self.draw(x, y, '@' as SHORT, conv.try_into().unwrap());
    //         }
    //     }
    //
    //     true
    // }

    pub fn start(self) {
        let game_loop = self.game_thread();
        let child = thread::spawn(move || {
            game_loop
        });

        let _child_handle = child.join();
    }
}

As @jameseb7 suggested, you can use an Option. update_function would become Option<Box<dyn FnMut(&mut OlcConsoleGameEngine)>> and you would call it like this:

fn on_user_update(&mut self) {
    if let Some(mut func) = self.update_function.take() {
        (func)(self);
        self.update_function = Some(func);
    }
}
2 Likes

Oh wow, OK yea I see what my issue was now. Earlier when I tried using an Option and match expression, I was forgetting to toss it back into the struct after having taken it out. @leudz and @jameseb7, thank you both!

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.