Skip to content

Instantly share code, notes, and snippets.

@DanielaE
Last active July 5, 2023 20:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielaE/39e270adb6ab27d12c61e1491d355d30 to your computer and use it in GitHub Desktop.
Save DanielaE/39e270adb6ab27d12c61e1491d355d30 to your computer and use it in GitHub Desktop.
Internal module partitions cause grief
// https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Modules.html
// gcc 12.2
# do NOT compile m-a.cpp, it will fail immediately!
g++ -std=c++2b -fmodules-ts -x c++ m-pif.cppm m.cppm m-b.cppm m-c.cppm main.cpp
// https://clang.llvm.org/docs/StandardCPlusPlusModules.html
// clang 15.0
clang++ -std=c++2b -fprebuilt-module-path=. m-pif.cppm m.cppm m-b.cppm m-c.cppm --precompile
clang++ -std=c++2b -fprebuilt-module-path=. -fmodule-file=m.pcm m-a.cpp m-b.pcm m-c.pcm m.pcm m-pif.pcm main.cpp
// msvc 17.5
cl /nologo /EHsc /std:c++latest /TP /c /interface m-pif.cppm m.cppm
cl /nologo /EHsc /std:c++latest /TP /c /internalPartition m-b.cppm m-c.cppm
cl /nologo /EHsc /std:c++latest main.cpp m-a.cpp m-b.obj m-c.obj m-pif.obj m.obj
// All compilers compile, link, and produce the "expected" result despite the program being ill-formed.
// For well-formedness add -DWANT-WELLFORMED
module m; // a module implementation unit, makes exported a@m(), b@m(), c@m() visible
// - is a module implementation unit [module.unit]/2
// - IMPLICITLY imports transitively partition :pif, [module.unit]/8 + [module.import]/7
// a@m()
// - is attached to module 'm' [module.unit]/7.3 + [basic.link]/10
// - is owned by module 'm'
// - 'corresponds' to a@m in partition :pif, [basic.scope]/4
// - IS the same entity as a@m with external linkage in partition :pif #1, [basic.link]/8
// -> has external linkage, [basic.link]/2.1 + [basic.link]/4.9
int a() {
return 42;
}
// TU not accepted by gcc,
// complains about contents of BMI (m.gcm), see below
// but accepts the same BMI for import into main.cpp
/*
$ g++ -std=c++2b -fmodules-ts -c m-a.cpp
In module imported at m-a.cpp:1:1:
m: error: failed to read compiled module: Bad file data
m: note: compiled module file is 'gcm.cache/m.gcm'
m: fatal error: returning to the gate for a mechanical issue
compilation terminated.
*/
module m:b; // an internal module partition that imports the module interface partition
// - is a module implementation unit [module.unit]/2
// - is a module partition [module.unit]/3
// - EXPLICITLY imports partition :pif, [module.import]/4
import :pif; // #1 // make exported a@m(), b@m(), c@m() visible
// b@m()
// - is attached to module 'm' [module.unit]/7.3 + [basic.link]/10
// - is owned by module 'm'
// - 'corresponds' to b@m in partition :pif, [basic.scope]/4
// - IS the same entity as b@m with external linkage in partition :pif #2, [basic.link]/8
// -> has external linkage, [basic.link]/2.1 + [basic.link]/4.9
#ifndef _MSC_VER // not accepted by msvc if import declaration #1 is present 😮
int b() {
return 42;
}
#endif
/* msvc chokes on the attempt to compile function b@m() like this
m-b.cppm(7,9): error C7619: cannot export 'b' as module partition 'b' does not
contribute to the exported interface of module unit 'm'
m-b.cppm(7,9): error C7619: int b() {
m-b.cppm(7,9): error C7619:
*/
module m:c; // an internal module partition
// - is a module implementation unit [module.unit]/2
// - is a module partition [module.unit]/3
// none of a@m(), b@m(), c@m() are visible
#ifndef WANT_WELLFORMED // sheer presence of c() in translation phase 9 makes program IF-NDR
// c@m()
// - is attached to module 'm' [module.unit]/7.3 + [basic.link]/10
// - is owned by module 'm'
// - is NOT exported
// - has module linkage, [basic.link]/2.2 + [basic.link]/4.8
// - 'corresponds' to c@m in partition :pif, [basic.scope]/4
// - is NOT the same entity as c@m with external linkage in partition :pif #3, [basic.link]/8
// - is NOT an ODR violation
int c() {
return 42;
}
#endif
export module m:pif; // a module interface partition unit
// - is a module interface unit [module.unit]/2
// - is a module partition [module.unit]/3
// all function declarations
// - are attached to module 'm' [basic.link]/10
// - are owned by module 'm'
// - have external linkage, [basic.link]/2.1 + [basic.link]/4.9
export {
int a(); // a@m() entity #1
int b(); // b@m() entity #2
int c(); // c@m() entity #3
}
export module m;
// - is the primary module interface unit [module.unit]/2
export import :pif; // exports a@m(), b@m(), c@m()
// interface dependency on partition :pif
// no interface dependency on partitions :b and :c
import m; // make exported a@m(), b@m(), c@m() visible
int main() {
int rc = 0;
#if !defined(__GNUC__) || defined(__clang__)
rc += a();
#endif
#ifndef _MSC_VER
rc += b();
#endif
#ifndef WANT_WELLFORMED
rc += c();
#endif
return rc;
}
@DanielaE
Copy link
Author

The two module units 'm-a.cpp' and 'm-b.cppm' are basically the same (besides the names of the function definitions).
Both:

  • are module implementation units
  • import module interface partition :pif
  • declare and define the same entity (a function) that they import from partition :pif

The standard doesn't differentiate between module partition TUs and non-partition TUs when it comes to module implementation units and the entities within the module purview. They are supposed to be treated the same.

Therefore, msvc should accept function b@m() in TU m-b.cppm.

@DanielaE
Copy link
Author

There are actually two problems in msvc here, the second one is:

The linker accepts symbol ?c@@YAHXZ::<!m> (int __cdecl c(void)) created from the compilation of non-exported function int c() with module linkage in TU m-c.cppm as a substitute for symbol ?c@@YAHXZ::<!m> (int __cdecl c(void)) requested from the compilation of function call c() with external linkage (transitively imported from module interface partition :pif in module m)

The .modmeta sections in the respective object files contain the necessary info to differentiate between these two C++ entities with incompatible linkage but identical mangling. The linker should refuse linking in this case, the program as shown is ill-formed!
Maybe this should also be forwarded to the linker team.

@DanielaE
Copy link
Author

Both clang and gcc share this linkage issue with msvc. So at least we don't have implementation divergence here 😊

@iains
Copy link

iains commented Jul 5, 2023

FWIW GCC 12.3+ and 13+ seems to have a fix for the m-a problem.

@DanielaE
Copy link
Author

DanielaE commented Jul 5, 2023

Most wonderful! 🚀✨
I didn't bother to recheck since back then.

@iains
Copy link

iains commented Jul 5, 2023

hmmm... m-c is an interesting problem .. I do not know if we have a mechanism at present for rejecting it (but it's NDR anyway) ..
.. if we did, AFAICS, we'd probably have to modify all the linkers out there, and I guess that is one reason that the design we have at present is how it is. Will have to think more about it - it does not seem (at present) that the compilers are doing anything 'wrong'.

@DanielaE
Copy link
Author

DanielaE commented Jul 5, 2023

Right, it's material to make users aware of and teach thoroughly so that they don't fall unwittingly into this pit of failure. This isn't obvious in any way and probably very difficult to convey to everyday users.

@iains
Copy link

iains commented Jul 5, 2023

well, philosophically, in m and m:pif the module author has promised that the three functions exist (so the consumers of the interfaces have to believe it). That promise turns out to be a "white lie" (or a typo even missing the :pif in m-c.cpp).

m:b and m:c both produce functions with the correct signature (the exportedness of b is only significant within the compiler, by the time the code is emitted the mangling of b@m is the same as c@m).

we have no mechanism to check that the module is "complete" (in an analogous manner to checking that a class impl. is complete) .. and IDK if it's even possible.

The linker (as things stand) cannot tell the difference between the b@m and c@m cases and therefore links happily.

Thinking of ways to (a) prove that an implementation is complete (b) to differentiate exported-ness in the mangling (which would allow the linker to complain) might be a way forward ..

.. I have a sneaking suspicion that [b] here was tried in the modules-ts and then withdrawn in favour of the scheme we have now (but ICBW).

perhaps this is a strange corner-case (where the user made the correct thing by accident) - have we evidence that people have made this mistake in the wild?

edit: I actually checked that the AST clang produces has the correct linkage for m-b and m-c.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment