I had a similar problem as you with almost the same setting - Game as dll and Engine as exe. Here are some notes on how to tackle this problem.
Call only virtual methods. As you pointed out, if the method you call is not declared virtual, the linker tries to find an implementation for it and fails (if it's not in the header - a thing we try to avoid). The method does not need to be abstract, virtual is enough. Also, note that in your struct Renderer
you can have methods that are not virtual, as long as you don't call them from the dll (if you do, the linker complains). It is probably not advisable to have such an interface, it would be much better to have some sort of API class which has only virtual public methods so users of this class cannot make a mistake.
All classes used from the dll need to be shared or header only. What I mean by this is, that as far as I know, there is no magic way, to have classes declared in header, implemented in cpp which is compiled to the exe and then use these classes from the dll. E.g., if you have a custom string class, it needs to be in a shared library. If it's just in the exe you will not be able to instantiate it in the dll (return it from functions etc.). A solution to this is to use header-only classes. E.g., your string may be implemented in a header in the Editor project and this header may be included by your Game project. This way you essentially compile the same code to both exe and dll.
To see a small working example see my repository with VS 2017 solution which demonstrates this exact problem and nothing else. repo link.
Much larger working example of this problem can be seen in idTech4 engine - DOOM 3 version here. It also uses a game as a dll and an engine as an exe. And also needs to exchange pointers to the engine's systems which are used from the game. The project is big, but if you take a look at project Game-d3xp
class Game.h
all the way down, they have the game's API with a single function GetGameAPI_t
which expects to get gameImport_t
struct with pointers to engine systems and returns gameExport_t
with game informations. The loading then happens in the Common.cpp
As you can see they use shared library idLib
in the respective project for things such as idString
. All engine classes used from the dll are usually very small and implemented in headers only (they are mostly structs).
Note that id themselves are moving away from this architecture and even their latest version of DOOM 3 - DOOM 3 BFG edition compiles to a single exe and the modules are static libraries instead of dlls.