1

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
  • 1,841
  • 11
  • 21
  • 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

2 Answers2

1

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.

darkblueflow
  • 11
  • 2
  • 3
  • 2
    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
  • 1
    Thanks. It would be great to have some official doc from Microsoft to confirm this. – Touloudou Oct 06 '21 at 07:25
1

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
  • 893
  • 7
  • 20