Good way to avoid virtual functions when implementing engine contexts and hot code reloading

Hi there,

so I realize virtual functions are a very contentious topic here.


I just recently added a feature to my engine where I can hot reload C++ game code while the engine i still running in the background (small demo https://twitter.com/RicoTweet/status/1392135424324345866).

In order to be able to do that, basically I compile my game as a dll with no outside dependencies.

To be able for my dll to communicate with my engine I provide one global function set_context that simply sets a global context pointer like that:

1
2
static Context* context = nullptr;
extern "C" __declspec(dllexport) void set_context(Context* new_context) { context = new_context; }


Context is a pure virtual interface that looks kinda like that

1
2
3
4
struct Context {
  virtual ~Context() {}
  virtual void draw_cube(vec3f pos, vec3f size) = 0;
};


Now whenever I change and recompile my game dll I load my library and call set_context where I set the pointer to my engine context.



This works very well


Now my question: is there a way to improve this approach avoiding the virtual function calls?

Honestly even when I don't use virtual functions, I would still have to design my context in a way that it basically is just a collection of function pointers, which factually would make no difference.

In the long run I would have to add static linking without dlls anyway, when I wanna build my games for something other than a PC anyway.


Feedback is appreciated.

~Rico

Edited by Rico on Reason: Initial post
Problems might come if the program gets old and a new version of the compiler changes how classes store their virtual function table or something. This is why most dynamically linked interfaces are procedural with manually packed structures, so that the caller will have the same binary interface as the function being called. When each atomic type has a fixed size (uint16_t, int64_t...) and is aligned by its own size in power-of-two sized structures, it's unlikely for different compiler versions to pack the data differently because it cannot be more aligned. COM based ActiveX components only allow fixed size atomic types in the interfaces and have specific unmangled C names for registration methods to keep the interface compiler independent, but it still turned into legacy features when computers changed to 64 bits.

If the hot reloading is only for speeding up development, any trick that works for now is justified if it can be removed when bugs start to pile up from over-engineering, which is usually the limiting factor for how advanced games become. I often use the Strategy/Policy pattern for algorithms, so that i can quickly swap out modules at runtime and compare different versions before I hard-code one of them for the final release. Maybe you can wrap that into a reusable development tool and let the developer select compilation with or without hot reloading.

For game modifications, it might be good to run level/campaign specific things as byte-code (potentially JIT compiled), so that ports of the game to new platforms in the future can use existing modifications made by players. This also allow some degree of safety against malware attacks if you let others compile modifications into a restricted format without direct access to system calls, memory and file system.

Edited by Dawoodoz on
You can use memory "command buffers" to pass information back to your engine. Without calling any function pointer at all.
Instead of calling function for every single tiny thing, you write commands to memory, and pass it back - either return it, or write to memory address that was passed to you.
For example, your draw_cube command in memory might look like this:
1
2
3
4
5
6
struct cube_command
{
  int id;
  vec3f pos;
  vec3f size;
};

You fill this structure - set id to something that identifies cube command (like 1), and use different id for different commands. Then set other members - pos & size. And memcpy into that memory buffer. Append other cummends afterwards.

And when engine needs to execute commands, it just reads this memory sequentially and does whatever it needs to do. Like it reads id first. Checks is it 1 - if yes, then it reads rest of cube_command - pos and size. And then runs functionality for your draw_cube. Then continues with parsing rest of memory.

Edited by Mārtiņš Možeiko on
@ Dawoodoz

Thanks for the input!

I assume that the engine and the game dlls are always compiled with the same compiler for now.

I also don't care to much about security. Using bytecode would kinda go against improving performance imho. Also Jitting is a no go for console development.

Edited by Rico on
@ mmozeiko

That with the command buffer is a nice idea! This didn't came across my mind.

However I belief that becomes problematic as soon when I need something that returns a value because then I need a immediate response from engine and can't wait for the next circle after the command buffer has been parsed.

Edited by Rico on
You can have the command buffers and have the engine expose some functions for other things (hopefully a small number).

A small detail, in the video on your tweet when you compile the dll, you seem to call vcvarsall.bat every time. Unless the console environment variable are cleared every time, it's not necessary to call it every time, and might even cause an issue as the PATH variable will expand each time and at some time will reach the character limit for a variable. And not calling it every time will save a little bit of time.
@ mrmixer
Thanks for the tip! The video is a bit older, I changed my environment already where I call vcvarsall just once...