Reputation: 2223
There is very little documentation online on the proper use of C++20 modules in shared libraries. Many folks are clearly interested, but I haven't been able to find a clear solution.
In MSVC, you need to use dllexport
when compiling the library, and dllimport
when consuming the symbols. This can be done using macros in "legacy C++", but this does not work with C++20 modules, since the code is only compiled once, regardless of preprocessor directives.
This post suggests that you only need to use dllexport
now, and that dllimport
will be taken care of automatically by the compiler. However, this comes from a comment which has now been deleted, and I couldn't find any reliable source on the topic.
How is one expected to create a shared library using C++20 modules?
Upvotes: 11
Views: 5992
Reputation: 206
Here's the answer for Visual Studio v17.5+. Note that I'm fairly new to C++ (I develop in C# professionally) so this may not be the ideal solution.
In the project properties for the library containing the module you wish to export, navigate to Configuration Properties > VC++ Directories. Ensure that "All Modules are Public" is set to "Yes".
Now, as long as your symbols are decorated with __declspec(dllexport), in the project that consumes your shared library the dll export will be automatically reinterpreted as dllimport on the consuming side automatically.
Upvotes: 1
Reputation: 51
C++20 modules have no special relationship with shared libraries. They are primarily a replacement of header files.
This means that you would develop a shared library with C++20 modules in a similar fashion as you would with header files before C++20, at least with my current understanding. You design some API that is exported (unfortunately still using vendor-specific attributes like __declspec(dllexport)
or __attribute__((visibility("default")))
) and implement it. You build your shared library file (.dll/.so) and an import library for distribution, same way as before. However instead of distributing header files, you would distribute module interface units instead. Module interface units are files containing an export module ABC;
declaration at the top.
And executables consuming that shared library would then import that module using import ABC;
, instead of #include
-ing a header file.
Edit: As was pointed out in the comments, it is seemingly still necessary on Windows to provide a macro switch inside the module interfaces that toggles between dllexport and dllimport attributes, similar to as it is done with headers. However, I have currently not experimented with this and can only defer to what @jeremyong has experimented with in What is the expected relation of C++ modules and dynamic linkage?.
Upvotes: 5
Reputation: 1578
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.
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.
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:
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;
}
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;
}
}
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 [email protected]
$(GCC) -shared [email protected] -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
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).
Upvotes: 5