Disabling inline compilation (Clang with VS2019)

Hi there - I first asked this question over on clang-users but I got advised to ask it here (sorry about the length...)

I'm a VS2019 user and I've been trying to switch it here to use Clang as the compiler, rather than MSVC. But I seem to have hit a common problem. Consider the following code:-

#if defined (BUILDING_DLL)
   #define DLL_API __declspec(dllexport)
#else
   #define DLL_API __declspec(dllimport)
#endif

namespace Gtkmm2ext {

   class DLL_API Keyboard
   {
     public:
       Keyboard ();
       ~Keyboard ();

       static Keyboard& get_keyboard() { return *_the_keyboard; }

     protected:
       static Keyboard* _the_keyboard;
   };

} /* namespace */

The above code is from a DLL which gets used by an exe. The DLL compiles and links just fine and the exe compiles. But when I try to link the exe, Clang's linker complains that it can't find '_the_keyboard'

But here's the thing... '_the_keyboard' is an internal variable that's private to the DLL. It should never need to get accessed by the exe. If I change 'get_keyboard()' to be just a declaration (and then implement it in a DLL source file) Clang is then happy - but unfortunately, this is one of several hundred similar linker errors.

So I'm wondering if (maybe) the compiler implemented its call to 'get_keyboard()' as inline code, rather than importing it from the DLL? Maybe for very simple code like this, Clang is trying to be clever and implement stuff inline if it can?

VS2019 can disable inline code via a compiler option called "/Ob0" - and typing "clang-cl /?" indicates that "/Ob0" is supported. But I still see the error, even if I specify "/Ob0" during compilation.

So will "/Ob0" disable all inline compilation for Clang? Or does it only take effect where there's an actual 'inline' keyword? Hope that all makes sense...

John

As far as I understand it, if you specify __declspec(dllexport) for a whole class, then *all* members, including statics, get explicitly exported in the DLL image.

I.e. it is as if you had manually specified __declspec(dllexport) to each and every member. I don't know if there is any way to override that for just a single class member, for example as with gcc you could add a __attribute__((visibility("hidden"))) to one of them.

So maybe you can selectively add DLL_API to those class members that you need exported, as described here:
https://docs.microsoft.com/en-us/cpp/cpp/using-dllimport-and-dllexport-in-cpp-classes?view=msvc-160#_pluslang_using_dllimport_and_dllexport_in_c2b2bselectivememberimportexport

-Dimitry

Thanks Dimitry - but why would that cause a problem? ''_the_keyboard" should never be needed by the exe - so why would the linker complain that it can't find something which it doesn't need anyway?

John

I’ve transferred John’s example into Compiler Explorer, which will hopefully be a useful demonstration. Both clang and MSVC handle the dllexport case identically… but they do differ in the dllimport case: https://godbolt.org/z/vTrEevY8o

Just as John suggested, disassembling the output shows that clang is inlining this function, even at -O1, where MSVC doesn’t, even at /O3.

I’d agree that this isn’t a great match, though it is a point of ambiguity in the handling of __declspec(dllimport).

One possible workaround: add a noinline marker on functions defined in headers that need to access protected members. (https://godbolt.org/z/Pe67ovMnP)
IMO, the best option would be to just adopt the policy that you don’t define DLL_API functions in headers… which I would suggest is the best option anyway, since you don’t want your importing code to have the definition.

Either way, we should probably decide how we want clang to behave in this case. It’s possible we should treat anything marked as __declspec(dllimport) as defined externally, and ignore any definition present in the source code?

Many thanks Eric,

I'm not quite sure what the difference is between LLVM and Clang so sorry if I'm getting them mixed up - but is there a possible fix by just extending the implementation for your "/Ob0" handler? In other words, if "/Ob0" gets specified during compilation, the compiler would stop trying to inline things?

John

I'm not quite sure what the difference is between LLVM and Clang so
sorry if I'm getting them mixed up - but is there a possible fix by just
extending the implementation for your "/Ob0" handler? In other words,
if "/Ob0" gets specified during compilation, the compiler would stop
trying to inline things?

It looks like the driver does translate "/Ob0" to the internal option
"-fno-inline" which ought to prevent inlining of anything that is not
marked as always-inline; I am pretty sure that simply being defined
in-class does not qualify. I tried your example on my machine locally
(adding a function that calls get_keyboard) and it did not inline the
call. I don't know why this result differs from Eric's.
--paulr

Thanks Paul - does Clang produce some log file that'd tell me what command got received from Visual Studio? VS tells me that it's sending "/Ob0" but if it's working for you, it'd be nice to check here if "/Ob0" actually got received. I've known things to stop working occasionally in new VS revisions.

John

I don't see a way to make Clang echo the original command line. You can
add -v to the command line, which will report the internal command line
passed from the driver to the compiler proper, and with /Ob0 you should
see -fno-inline on that command line.
--paulr

Yes that works. So it's strange that the inlining seems to be different for us (unless it's some other project setting??)

John

Anyway, ignoring the "/Ob0" issue for a minute, maybe there's a better solution available...

Could compilations be adapted so that wherever a symbol is found to be declared as __declspec(dllimport) there's an automatic assumption that it shouldn't be inlined?

John

Anyway, ignoring the "/Ob0" issue for a minute, maybe there's a better
solution available...

Could compilations be adapted so that wherever a symbol is found to be
declared as __declspec(dllimport) there's an automatic assumption that
it shouldn't be inlined?

+rnk who is much better in tune with MSVC compatibility than I am.

In general, Clang is willing to inline a function defined in-class
because of the One Definition Rule; C++ says we can assume that the
definition is the same everywhere. As I said, I don't see Clang
inlining the method, but Godbolt does, I don't know what the difference
is there.

In the specific example, moving the method definition out of the class
should avoid the problem; but if you have a large number of methods
like this, that's a real lot of work and it would be nice to find some
other solution.

If MSVC promises not to inline a dllimport method, that seems like
something Clang should take into consideration in MS-compatibility
mode. Probably implicitly marking dllimport methods as noinline would
be sufficient, but I think we'd be willing to do that only if MSVC
actually makes that promise explicitly.
--paulr

I don't believe MSVC makes such a promise. Here's a simple example:

inline int __declspec(dllimport) foo() { return 42; }
int f() { return foo(); }

MSVC will inline the call to foo() here (https://godbolt.org/z/jKWqEjKG4).

But, it seems something changed between MSVC 19.21 and 19.22. Consider
this example:

extern int __declspec(dllimport) x;
inline int __declspec(dllimport) get_x() { return x; }
int f() { return get_x(); }

int __declspec(dllimport) foo();
inline int __declspec(dllimport) bar() { return foo(); }
int g() { return bar(); }

MSVC 19.21 will inline the get_x() and bar() calls, but 19.22 will
not: https://godbolt.org/z/n1cedv3a8

Clang matches the 19.21 behaviour. The way it checks whether a
dllimport function is safe to inline is whether it only references
symbols which are also dllimport (the logic lives in
CodeGenModule::shouldEmitFunction()).

It seems that MSVC has become more conservative here. We could update
Clang to behave similarly, but it would be good to understand the
exact motivation.

> If MSVC promises not to inline a dllimport method, that seems like
> something Clang should take into consideration in MS-compatibility
> mode. Probably implicitly marking dllimport methods as noinline would
> be sufficient, but I think we'd be willing to do that only if MSVC
> actually makes that promise explicitly.
> --paulr

I don't believe MSVC makes such a promise

Actually, MSDN (https://docs.microsoft.com/en-us/cpp/cpp/defining-inline-cpp-functions-with-dllexport-and-dllimport)
documents the opposite:

"You can also define as inline a function declared with the dllimport
attribute. In this case, the function can be expanded (subject to /Ob
specifications)"

Of course the reality is more complicated, but it's clear MSVC never
excluded dllimport functions from inlining in general.

On the other hand, the experience described upthread is that Clang's
handling of /Ob0 still permits inlining. The MSDN quote suggests that
/Ob0 should suppress inlining, and that's not what people see.
--paulr

Many thanks for the contributions here. I often post on the MS Developer forum so I'll ask for clarification there - and I'll post back if I get any replies (though bear in mind that replies can sometimes take a long time there!!)

I've used MSVC for maybe 20 years and I've never once known a DLL function to be inline-able. So my guess (though it is just a guess) is that something maybe went awry in 19.21 and then got corrected in 19.22. Best regards,

John

Ah... maybe that's the situation - perhaps a DLL function CAN be inlined but only if it's specifically declared as such? That makes a lot of sense and it would also explain why I've never seen it here with ordinary DLL declarations (i.e. those not declared using inline)

John

I haven't been able to reproduce that though. When I try it,
clang-cl's /Ob0 correctly suppresses the inlining:

> extern int __declspec(dllimport) x;
> inline int __declspec(dllimport) get_x() { return x; }
> int f() { return get_x(); }
>
> int __declspec(dllimport) foo();
> inline int __declspec(dllimport) bar() { return foo(); }
> int g() { return bar(); }
>
> MSVC 19.21 will inline the get_x() and bar() calls, but 19.22 will
> not: https://godbolt.org/z/n1cedv3a8
>
> Clang matches the 19.21 behaviour. The way it checks whether a
> dllimport function is safe to inline is whether it only references
> symbols which are also dllimport (the logic lives in
> CodeGenModule::shouldEmitFunction()).
>
> It seems that MSVC has become more conservative here. We could update
> Clang to behave similarly, but it would be good to understand the
> exact motivation.
>

Many thanks for the contributions here. I often post on the MS
Developer forum so I'll ask for clarification there - and I'll post back
if I get any replies (though bear in mind that replies can sometimes
take a long time there!!)

Thanks! It would be interesting to learn why this changed. (I can only
come up with obscure reasons right now.)

I've used MSVC for maybe 20 years and I've never once known a DLL
function to be inline-able. So my guess (though it is just a guess) is
that something maybe went awry in 19.21 and then got corrected in
19.22. Best regards,

On the godbolt example above, I was able to try back to MSVC 19.14 (I
think that's Visual Studio 2017 version 15.7) and it also shows
inlining. Same with Visual Studio 2013 which is the oldest version I
have locally.

I think generally developers shouldn't notice this though, since both
MSVC and Clang are careful only to inline dllimport functions when
they only reference symbols which are also dllimport, and things
should just work. For that reason, I don't understand why your code
isn't working. The way it's written, _the_keyboard should get exported
from the DLL, and the fact that it's not is a mystery.

If you could provide an example with code both for building the DLL
and EXE which reproduces the link error (even if the DLL has to be
built with MSVC and the exe with Clang), that would be useful.

Thanks,
Hans

perhaps a DLL function CAN be inlined but only if it's specifically declared as such? That makes a lot of sense and it would also explain why I've never seen it here with ordinary DLL declarations (i.e. those not declared using inline)

I found this Microsoft document about inlining:-

https://docs.microsoft.com/en-us/cpp/cpp/inline-functions-cpp?view=msvc-160

About half way down there are two examples and the 2nd one states that class member functions which get implemented in a header file are now regarded as being implicitly inlined. This supports Paul's statement yesterday about the One Definition Rule - but what MS doesn't clarify is whether or not that'd also apply to DLL functions. Paul wrote:-

Aaaargh!! I just realised this was a spelling mistake at my end. Here's my original example...

#if defined (BUILDING_DLL)
  #define DLL_API __declspec(dllexport)
#else
  #define DLL_API __declspec(dllimport)
#endif

But because the library can be built for different platforms, the actual code here was slightly more complicated - looking like this:-

     #if defined (COMPILER_MSVC)
       #if defined (BUILDING_DLL)
         #define DLL_API __declspec(dllexport)
       #else
         #define DLL_API __declspec(dllimport)
       #endif
     #else
         #define DLL_API
     #endif

I'd changed the first line to read #if defined (COMPILER_MSVC) || defined (COMPILER_CLANG) - except that in the actual VS project file I'd then mis-spelled COMPILER_CLANG - so apologies and thanks to everyone who's helped here... I'd spent days looking through header files and source files without even thinking to check the VS project itself :frowning:

Fixing this has made it compile and link with optimizations disabled ('/Ob0') but I guess I'll need to check what happens once I enable them again. If header-defined functions are routinely treated as inlined, I guess the problem might come back again :frowning:

I suppose this brings up an obvious question... when building with MSVC I could have used the built-in preprocessor directive _MSC_VER rather than using my own COMPILER_MSVC - so does Clang have something built-in that's always similarly #defined? Or even better - does anyone know if there's some preprocessor directive that's always specified for both compilers (i.e. when they're being used with Visual Studio) ?

Thanks again, John