Is Link-Time Dependency Injection possible in any common linkers?

21 hours ago 1
ARTICLE AD BOX

I've been using Java and Google's Dagger2 dependency injection, and contemplating how to implement similar Dependency Injection in C++ with little-to-no overhead. One stumbling block I've found is when class A injects a FooEventDispatcher, so it can emit FooEvents to any and all listeners. And then class B can both implement the desired FooListener interface, and "register itself" with the FooEventDispatcher as listeners, to receive the event.

class FooListener { virtual ~FooListener(){}; virtual void onFoo(const FooEventData& event) const =0; }; DECLARE_LINK_TIME_LIST(FooListener, FooListenerList); class A { FooListenerList fooListeners; A(FooListenerList&& listeners): fooListeners(std::move(listeners)) {} void bar() { FooEventData event = {...}; // This should no-op if no FooListener was linked in the binary. fooListeners([event&](FooListener& listener){ listener.onFoo(event); }); } }; class B: FooListener { void onFoo(const FooEventData& event) const {...} }; LINK_TIME_REGISTER(FooListenerList, B);

Since A, and B only depend on FooEventDispatcher and the FooListener interface, and not on each other, they maintain full module separation, and can be tested separately from each other, without even compiling the other.

Except, C++ initializes statics before any method in that "translation unit" (aka Cpp file) is executed. But there's no method in B.cpp that would be executed until after it registers itself as a listener. So that means instead, I have to manually make a component module, that lists all of the implementations for all of the interfaces in that binary, including for each test binary, and the app initialization has to explicitly call this to initialize everything, which is wildly inconvenient.

Is there some neat trick that can be used in any of the common linkers (bfd, gold, lld, mold, wild, MS Link), for an implementation to "register itself" as an implementation of an interface, in some static global somehow? Ideally as a linked list, but at this point, even if it only allows one implementation, that's a lot better than nothing. Ideally, such a trick would even allow for inlining of the implementation's methods into callsites with Link Time Optimization, but I've lost faith in finding any such miracle.

When there's always exactly 1 implementation, then DECLARE_LINK_TIME_LIST(Type,Name) can simply be extern const Type& Name; and LINK_TIME_REGISTER(Name,Impl) can simply be const Impl impl; const Type& Name = impl;, and the linker handles that, but it doesn't work when there's 0, or multiple potential implementations in the same test/binary. Some sort of link-time-mutable linked-list, that is constant in the final binary.

MS Link's #pragma init_seg(".mine$m", myexit), and GCC/Clang's __attribute__((constructor)) allow an implementation to register itself at process start, which is functional, but unfortunate that it incurs a runtime startup penalty, and is incompatible with inlining. I suspect that weak references can handle the 0-1 case, but I don't see how to use that to build some sort of linked-list to handle the 2+ implementation case.

Read Entire Article