Thunks for class template member functions

Problem definition

Given the C++ header below...

#pragma clang lifetime_elision

template <typename T>
class MyTemplate {
 public:
  MyTemplate(T value) : value_(value) {}
  const T& GetValue() const;
 private:
  T value_;
};

using MyIntTemplate = MyTemplate<int>;

... Crubit will generate Rust bindings that can call into the MyTemplate<int>::GetValue() member function. To support such calls, Crubit has to generate a C++ thunk (to instantiate the class template and to provide a symbol for a C-ABI-compatible function that Rust can call into):

extern "C"  // <- C ABI
int const& __rust_thunk___ZNK10MyTemplateIiE8GetValueEv(
    const class MyTemplate<int>* __this) {
  return __this->GetValue();
}

There are other (non-template-related) scenarios that require generating thunks (e.g. inline functions, or functions that use a custom calling convention), but templates bring one extra requirement: a class template can be defined in one header (say my_template.h) and used in multiple other headers (e.g. library_foo/template_user1.h and library_bar/template_user2.h). Because of this, the same thunk might need to be present in multiple generated ..._rs_api_impl.cc files (e.g. in library_foo_rs_api_impl.cc and library_bar_rs_api_impl.cc). This may lead to duplicate symbol errors from the linker:

ld: error: duplicate symbol: __rust_thunk___ZNK10MyTemplateIiE8GetValueEv

Implemented solution: Encoding target name in the thunk name

One solution is to give each of the generated thunks a unique, target/library-specific name, e.g.: __rust_thunk___ZNK10MyTemplateIiE8GetValueEv__library_foo (note the library_foo suffix).

Pros:

  • Minimal extra code complexity (e.g. no need for templates-specific code in thunk-related code in src_code_gen.rs).
  • Obviously correct behavior-wise (e.g. since it is just like other thunks which we assume are implemented correctly).

Cons:

  • Performance guarantees are unclear. Binary size depends on link time optimization (LTO) recognizing that all the thunks are identical and deduplicating them.

    • This seems to work in practice (at least for production binaries).
    • Future work: add tests + consider asking LLVM to provide LTO guarantees
  • Requires escaping Bazel target names into valid C identifiers. See ConvertToCcIdentifier(const BazelLabel&) in bazel_types.cc.

Alternative solutions

Function template

An alternative solution would be to use a function template that we immediately explicitly instantiate. These still generate the code we need, but their duplicated symbol definitions (across multiple binding crates) won't cause an ODR violation. It is expected that a single function template is instantiated multiple times in multiple translation units, therefore the linker silently merges these equivalent definitions.

Example:

// Thunk is expressed as a function template:
template <typename = void>
__attribute__((__always_inline__)) int const&
__rust_thunk___ZNK10MyTemplateIiE8GetValueEv(
    const class MyTemplate<int>* __this) {
  return __this->GetValue();
}

// Explicit instantiation of the function template:
// (to generate a symbol that `..._rs_api.rs` can call into)
template int const& __rust_thunk___ZNK10MyTemplateIiE8GetValueEv(
    const class MyTemplate<int>* __this);

Pros:

  • Naturally deduplicated (just depending on what C++ already does for function templates).

Cons:

  • Assumes a particular ABI - a function template specialization uses the calling convention prescribed by the platform C++ ABI. We know that the Itanium ABI maps C++ sigatures to the C ABI and therefore will be compatible with the calling convention expected by the generated ..._rs_api.rs. Further research is needed to investigate the guarantees offered by other platforms (e.g., the MSVC ABI).
  • Requires extra complexity to calculate the mangled name of the function template specialization.
    • Crubit doesn’t have a clang::FunctionDecl corresponding to the function-template-based thunk, and therefore Crubit can’t use clang::MangleContext::mangleName to calculate the linkable/mangled name of the thunk.
    • Reimplementing clang::MangleContext::mangleName in Crubit seems fragile. One risk is bugs in Crubit's code that would make it behave differently from Clang (e.g. code review of the initial prototype identified that mangling compression was missing). Another risk is having to implement not just ItaniumMangleContext, but also MicrosoftMangleContext.
    • One idea to avoid reimpliementing mangling is to explicitly specify the name for the function template instantiation using __asm__("abc") (sadly this doesn't seem to work - it may be a Clang bug).

An abandoned prototype of this approach can be found in a (Google-internal) cl/450495903.

Explicit linkonce_odr attribute

Example:

extern "C"
int const& __rust_thunk___ZNK10MyTemplateIiE8GetValueEv(
    const class MyTemplate<int>* __this)
    __attribute__((linkonce_odr))  // <- THIS IS THE PROPOSAL
{
  return __this->GetValue();
}

Pros:

  • All the “pros” of the “Encoding target name in the thunk name” approach (simplicity + correctness of behavior)
  • All the “pros” of the “Function template” template approach (deduplication)

Cons:

  • Requires changing Clang to support the new attribute (e.g. requires convincing the Clang community that this is a language extension that is worth supporting). TODO(b/234889162): Send out a short RFC to gauge interest?

Rejected solutions

  • selectany doesn't work with functions, only data members. Furthermore, we need something that maps to linkonce_odr, and selectany maps only to linkonce.

  • __attribute__((weak)) has the disadvantage that a weak definition can be overridden by a strong one. This rule makes weak definitions non-inlineable except in full-program LTO. C++ function template instead follows the ODR rule that says that all definitions must be equivalent, making them inlineable.