Hi everyone,
Disclaimer: I asked similar question in googletest
’s issue 4699, but clang-tidy
seems to work a bit differently, so I ask here as well. Core part of the question is the same. I didn’t have a chance yet to dive deep into clang-tidy
implementation details, so if some of my assumptions/understanding is incorrect, please share how portable solution is achieved, I’d like to reuse this idea in another project.
As far as I can tell clang-tidy allows out-of-tree check-plugins loaded at runtime via --load
option. In this case I believe argument to --load
should be shared object / dynamic library that contains new check to be registered in clang-tidy
. According to documentation:
Developing an out-of-tree check as a plugin largely follows the steps outlined above, including creating a new module and doing the hacks to register the module.
“Hacks” here mean using “statically initialized variable” + anchor variable with volatile
qualifier.
One thing that concerns me is one cannot use volatile
trick as-is in case of out-of-tree plugin as it assumes modifying llvm-project
source files (clang-tidy/ClangTidyForceLinker.h
). However, locally on Ubuntu my out-of-tree plugin works even without volatile
anchor.
Another concern is this approach seems to use constructor implementation of a global object from separate translation unit as registration (correct me if I’m wrong).
However, as far as I can tell there’re multiple caveats with this approach.
C++ standard (I checked C++17 draft version, let me know if it’s not applicable, but I think it should; other C++ standards are likely aligned with this) does not guarantee dynamic initialization of non-local non-inline object with static storage duration will be executed prior to the first statement of main
function. It can be deferred until later than the first odr-use of the object in question. See C++17 draft 6.6.3 Dynamic initialization of non-local variables [basic.start.dynamic] page 68 (4). I also found related stackoverflow questions (45210561, 45018739). It seems to be a problem to me as the object isn’t odr-used - the only purpose of it to be initialized, so registration is triggered.
Doing my little research for a way to implement registration via global objects initialization, such that it reliably works with GCC, Clang and MSVC I found for GCC/Clang one could use __attribute__((constructor))
(see here) to guarantee initialization will happen before main
, but there seem to be no alternative for MSVC (see here). There exists some approach for MSVC using “user initializer section (.CRT$XCU)” (1113409), but I found an example where glib moves away from it because it’s not really portable (example).
Also, is there a potential risk of the object to be optimized away by the compiler (as unused)? Should some attributes be used here to avoid this? Is there something to be used with MSVC? (note: I saw GCC/Clang __attribute__((unused))
, but it seems to do the same thing as C++17 [[maybe_unused]] - just omitting a warning, not necessary keeping code executed).
So overall, is my understanding correct that clang-tidy
out-of-tree check-plugin registration relies on implementation-defined behavior that global object initialization happens before main and so in theory it may break, e.g. on MSVC, as there’re no direct way to instruct it do so?
Is the fact that dynamic loading (I assume something similar to dlopen) is used helps here? For example, from the link above it looks like glib
were migrating towards DllMain-based approach for MSVC.
In general, I have an impression there’re multiple widely used projects (googletest
, clang-tidy
) that use this approach with MSVC, so maybe it’s an example of behavior that’s not guaranteed by the C++ standard, but in practice supported by all major implementations?