9

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?

Touloudou
  • 2,079
  • 1
  • 17
  • 28
  • Even though you may be using the C++20 standard, might want to do a [feature test](https://en.cppreference.com/w/cpp/feature_test) just to make sure your compiler supports **modules**. – Eljay Oct 05 '21 at 15:01
  • I have been toying around with MSVC 16.11.1 for a while, and everything has been working fairly smoothly. Shared libraries are the first "major" hurdle so far. – Touloudou Oct 06 '21 at 07:19

3 Answers3

5

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?.

darkblueflow
  • 51
  • 2
  • 4
  • 3
    This does not answer the question. The module interface units were built using `__declspec(dllexport)` in those declarations. But to *import* a shader library function, you need a declaration that says `__declspec(dllimport)`. Does the module system understand that and automatically transform `dllexport` into `dllimport` when being imported, or do you have to build new modules with different declarations? – Nicol Bolas Oct 05 '21 at 16:13
  • 1
    @NicolBolas Yes, I should have adressed that. As far as I know the answer is yes, MSVC has to treat declspec-dllexports as dllimports when importing a module. And as far as I know it does this. – darkblueflow Oct 05 '21 at 16:19
  • 3
    Thanks. It would be great to have some official doc from Microsoft to confirm this. – Touloudou Oct 06 '21 at 07:25
  • @darkblueflow well see https://stackoverflow.com/questions/52286991/what-is-the-expected-relation-of-c-modules-and-dynamic-linkage/74444920#74444920 I actually tested things and things do not work the way you claim – jeremyong Nov 16 '22 at 04:28
  • I must have misinterpreted my early experiments with modules. I will update the answer. – darkblueflow Nov 17 '22 at 17:40
4

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).

alexpanter
  • 1,222
  • 10
  • 25
  • This no longer works on gcc 13 g++ -std=gnu++20 -fmodules-ts -xc++-system-header iostream cc1plus: fatal error: iostream: No such file or directory – Zopolis4 Sep 03 '22 at 06:23
  • @Zopolis4 That sounds like you are missing some library installments. Also - how did you get gcc 13? Looking at https://gcc.gnu.org/ it doesn't seem to exist.. On Linux you would likely need `libgcc-dev`. But honestly, if you do `sudo apt install gcc-12` (with a recent version of ubuntu), then you should be good to go. – alexpanter Sep 03 '22 at 16:58
  • I built gcc13 myself – Zopolis4 Sep 30 '22 at 02:28
  • @Zopolis4 I have poor experience with building gcc, especially for the part with installing it locally afterwards. I don't think I can be of help here. – alexpanter Oct 20 '22 at 09:33
2

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.

Todd Burch
  • 200
  • 8
  • Any sources on this? I have read this last paragraph of it working "automatically" at multiple locations but have yet to see an official statement from MS on this. How do you know it will work from 17.5 onward? – Excelcius Jun 09 '23 at 21:43
  • I'll quickly repeat here what I found out [there](https://stackoverflow.com/a/74444920/1314789): I can confirm that it seems to do the right thing but you need to have whole program optimization set to /GL, otherwise there will still be an indirection, which is not there when using `dllimport`. – Excelcius Jun 09 '23 at 22:04