Background
A translation unit which declares a module interface or a module partition will be treated as a module unit and will, when compiled, generate both an object file and a binary module interface (BMI).
The BMI is a binary representation of an abstract syntax tree, that is a data structure representing the syntax and data types of the program. We have the traditional C++ compilation pipeline:
program -> precompiler -> lexer -> parser -> assembler -> linker
With GCC, we should add the compiler flag -c
which tells the compiler to compile and assemble but not link.
But shared libraries are built by the linker by reading several compiled object files together and creating a shared object. So that happens after the BMI's have been built. And the BMI's may be built without linking them together as that is two different stages.
Module Visibility
In C# when building a DLL we have visibility attributes on class level, ie. public
, private
, internal
. In C++ we can obtain the same functionality with module partitions.
A module partition, declared with module <module> : <partition>;
will be entirely visible inside the compilation unit that declares export module <module>;
, but not outside that module. This reminds me of internal
mode from C#. But if we however export the partition with export module <module> : <partition>;
then its declarations will be publicly visible. Read more on cppreference.
Example
I have solved that problem with GCC (g++-11), see here.
In essence, you don't need DLL import/export since there are (likely) no headers involved. I have tried inserting these visibility attributes but with complaints from my compiler, so I guess we might not need them after all. Other than that, it's standard procedure. I copy/paste my example here as well:
Main
import <iostream>;
import mathlib;
int main()
{
int a = 5;
int b = 6;
std::cout << "a = " << a << ", b = " << b << '\n';
std::cout << "a+b = " << mathlib::add(a, b) << '\n';
std::cout << "a-b = " << mathlib::sub(a, b) << '\n';
std::cout << "a*b = " << mathlib::mul(a, b) << '\n';
std::cout << "a/b = " << mathlib::div(a, b) << '\n';
return 0;
}
Library
export module mathlib;
export namespace mathlib
{
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
}
Makefile
GCC=g++-11 -std=c++20 -fmodules-ts
APP=app
build: std_headers mathlib main
std_headers:
$(GCC) -xc++-system-header iostream
mathlib: mathlib.cpp
$(GCC) -c $< -o $@.o
$(GCC) -shared $@.o -o libmathlib.so
main: main.cpp
$(GCC) $< -o $(APP) -Xlinker ./libmathlib.so
clean:
@rm -rf gcm.cache/
@rm -f *.o
@rm -f $(APP)
@rm -f *.so
Running
g++-11 -std=c++20 -fmodules-ts -xc++-system-header iostream
g++-11 -std=c++20 -fmodules-ts -c mathlib.cpp -o mathlib.o
g++-11 -std=c++20 -fmodules-ts -shared mathlib.o -o libmathlib.so
g++-11 -std=c++20 -fmodules-ts main.cpp -o app -Xlinker ./libmathlib.so
./app
a = 5, b = 6
a+b = 11
a-b = -1
a*b = 30
a/b = 0
Now this is clearly platform-specific, but the approach should work on other platforms. I have tested a similar thing with Clang as well (same repo as linked).