1

Hi all and sorry for the cryptic title.

I have an executable which loads a DLL using WinAPI LoadLibrary/GetProcAddress. The program calls an exported function with C linkage with a custom class as parameter (as a reference but I will come back later on this). The exported function is defined as follow (GGen will be defined just after):

// LIB_API is __declspec(dllexport) in DLL context and
// __declspec(dllimport) in executable context
LIB_API void dbgt_generate(IRFile& cr) {
  GGen::generate(cr);
}

My custom class is defined as follow and is header-only:

class IRFile {
public:
  std::string identifier;

  using callback_t = std::string(*)(IRFile*, void*);
  static callback_t _callback;

  virtual std::string buildFile(void* genData) {
    assert(IRFile::_callback);
    return IRFile::_callback(this, genData);
  }
};

inline IRFile::callback_t IRFile::_callback = nullptr;

The exported function calls a singleton class by the way of a static member (the generate method):

static std::string cbFile(IRFile* _this, void* r) {
  return "irfile";
}

class GGen {
public:
  GGen() noexcept {
    IRFile::_callback = cbFile;
  }

  static void generate(IRFile& cr) {
    static GGen ggen;
    assert(IRFile::_callback);
    auto data = cr.buildFile(nullptr);
    assert(!data.empty());
  }
};

The executable simply initializes an IRFile instance on the stack and calls the exported function. My problem is that GGen::generate works well and assert pass without any problem but when buildFile is called during GGen::generate, the assert in it does not pass. At the precise cr.buildFile call moment, IRFile::_callback appears to be NULL.

After investigations, it appears that IRFile::_callback "has another address when in the virtual function compared to before" (I know this sound weird). I have figured out that removing virtual does fix the problem but in my complete program, I need it even if not used here. Another point is that when I pass the IRFile instance to the exported function as a copy (pass-by-value) the problem is also gone. But I don't understand why this last point fixes the problem as even if memory sharing was problematic, IRFile::_callback is initialized inside and by the DLL itself... I have added the identifier field to ensure correct memory is used and it is the case (in other words, this is correct).

Is it stack corruption? Why does removing virtual changes anything as, in my knowledges, static has nothing to do with vtables? Is this the effect of an undefined behavior with coincidences?

Please don't mind ugly C++ or naming conventions, I had to rewrite parts for testing to simplify.

[Edit 1] Clarified that the problem occurs after IRFile::_callback is initialized by GGen::generate and indirectly GGen ctor.

[Edit 2] As suggested in comments, I have tried to inline IRFile::_callback and export it as DLL symbol without success. I have also tried to put IRFile in a separate project (.lib and .dll tested) inlined or not, defined in a .cpp or not. I have also tried to encapsulate calls to _callback with setters/getters without success event when in a separate project (.lib/.dll).

[Edit 3] Added inline specifier to the _callback definition in header file to avoid multiple definition of _callback according to C++17:

kilian3008
  • 21
  • 3
  • I think you need to use `__declspec(dllexport)` declaration when you declaring class `IRFile` when building DLL. Or may be just for `IRFile::_callback` declaration/definition, I'm not sure... Otherwise DLL's `IRFile::_callback` and EXEs `IRFile::_callback` will not be same, and that is what you see probably. – sklott Dec 27 '22 at 15:04
  • Most likely you have 2 *\_callback* instances, one in the *.dll*, and one in the *.exe*. What if `inline static callback_t _callback = nullptr;` (and remove the outside class definition)? https://stackoverflow.com/q/2479784/4788546. – CristiFati Dec 27 '22 at 15:52
  • Thanks for both your suggestions. For sklott one, I couldn't mark the class as exported because it would cause problems against linking the constructor (as class is header-only shared one). Also, marking _callback as exported did not fix the problem. For CristiFati one, unfortunately it didn't change anything on the behavior of the code... Following this, I have tried some other things as marked in Edit 2. – kilian3008 Dec 27 '22 at 17:09
  • `IRFile::callback_t IRFile::_callback;` This is not header-only. You have a definition in every TU that includes this header, which is an ODR violation. – n. m. could be an AI Dec 27 '22 at 18:25
  • Thanks for your comment, I have added (and tested of course) inline in the `IRFile::_callback` definition as C++17 permits with inline variables to ensure only one definition is picked up. Unfortunately, this didn't changed the behavior of the code. – kilian3008 Dec 27 '22 at 20:24
  • `inline` variables do **not** work like they should across DLL boundaries. According to the standard, there is one copy per program, but in practice each DLL has its own copy. – n. m. could be an AI Dec 27 '22 at 21:22
  • Did you try the obvious approach: defining the member in a (1 line) source file (from the *.dll*)? Also, what compiler are you using? – CristiFati Dec 28 '22 at 08:51
  • I didn't know that concerning inline variables... I am using MSVC latest version. I have put `IRFile` in a separate .lib, the `_callback` definition in a .cpp inside the .lib. The definition is set to a stub function. When I run, I can see that before the call to `buildFile`, `_callback` is the DLL's one pointing to `cbFile` but when I enter it, `_callback` is the executable's one pointing to the stub... How can I make it one? Also, why is it only the case when member declared virtual? – kilian3008 Dec 28 '22 at 11:06
  • Please share the exact configuration of the *.dll* and the *.exe* (each file with its code), cause that could be important. [\[SO\]: How to create a Minimal, Reproducible Example (reprex (mcve))](https://stackoverflow.com/help/minimal-reproducible-example). – CristiFati Jan 05 '23 at 10:39

0 Answers0