This is a very real problem, particularly the M:N issue.
But I think it's inevitable you have to do things on both sides of the language boundary, i.e. pushing them towards a lowest common denominator. (e.g. Microsoft's COM, which actually worked pretty well)
Really what it pushes you toward is wire protocols and NOT relying on the type system. That is the C and C++ ABIs are related to but different than the APIs (type system).
My experience tells me that transparent interop is kind of a pipe dream. The problems always pop up somewhere. By "transparent", I meant "Rust function calls C++ function" and "C++ function calls Rust function" without other metadata/bindings. The codegen becomes a big problem in practice.
Fundamentally a lot of languages LOOK the same but they ACT completely differently. A Rust function is not a C++ function is not a Python function is not a Go function. (And funny thing -- as of C++ 11, C++ now has many different notions of "function", because of move semantics).
And functions are actually "easy" compared to types (e.g. inheritance vs. typeclasses vs. interfaces)
----
Basically I would say the problem is that you either spend time manually wrapping or annotating your code (as you say), OR you put an ever growing list of heuristics in the code generator, a la SWIG.
Those heuristics have bugs, and will make your program unreliable. They're also "someone else's problem", which leads app developers to come up with horrific workarounds.
So I go with the simple manual wrapping, and reducing the number of things to wrap by changing the structure of your program. This also has other benefits like efficiency, i.e. crossing the language boundary less often.
(Related: I think better build systems can go along way toward solving this problem. Unfortuately there seem to be a lot of language-specific build systems and package managers now, which only exacerbates the interop problem. If you have one language-neutral build system, it's not that bad.)