Bidirectional C++ Interop within the Same Target in CMake

I have this tiny project:

- CppInterop
-- main.swift
-- helper.hpp
-- helper.cpp
-- CMakeLists.txt

Here are the contents of swift and cpp files:

// helper.hpp
#pragma once

int add_numbers(int a, int b);
// helper.cpp
#include "helper.hpp"

int add_numbers(int a, int b) {
    return a + b;
}
let a = add_numbers(1, 2)

print("Hello, World!: \(a)")

I am struggling to complete the CMakeLists.txt file to compile this properly. I am looking at https://github.com/swiftlang/swift-cmake-examples , but it is calling the cpp in the swift file from a separate target, and I do not want that.

Yet calling the compiler like this directly works perfectly:

swiftc \
        -import-objc-header helper.hpp \
        -cxx-interoperability-mode=default \
        main.swift helper.cpp

No matter what I think of, add_numbers is not found during the compilation.

I can make it compile with cmake by referencing the helper header through target_compile_options, but then the editor still thinks that add_numbers is not found.

I feel so stupid. Can anyone help with this please?

What does your CMakeLists.txt look like? That might give some clues…

I am looking for the smallest working configuration first:

cmake_minimum_required(VERSION 4.0)
project(CppInterop LANGUAGES CXX Swift)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_Swift_LANGUAGE_VERSION 6)

add_executable(CppInterop main.swift helper.cpp)

target_include_directories(CppInterop PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

target_compile_options(CppInterop PUBLIC "$<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default>")

# target_compile_options(CppInterop PUBLIC "$<$<COMPILE_LANGUAGE:Swift>:-import-objc-header>")
# target_compile_options(CppInterop PUBLIC "$<$<COMPILE_LANGUAGE:Swift>:${CMAKE_CURRENT_SOURCE_DIR}/helper.hpp>")

If I uncomment the last two lines, the project compiles but the editors still do not recognise add_numbers function in the main file with the same message: Cannot find 'add_numbers' in scope.

I’m not convinced that just adding 2 source files to a single executable target with cmake will actually compile them together like what is being done with swiftc. I would expect/think that the .cpp file would be compiled with something like gcc, while the .swift file would be compiled with swiftc. Can someone else confirm this?

Unless there’d be a way to set the compiler for .cpp to swiftc manually…

You'll want a modulemap to tell Swift how it should import your files as a single module (you only have one header, so it's pretty simple):

// module.modulemap
module CppInterop {
  header "helper.hpp"
  requires cplusplus
}

Then the target_include_directories(CppInterop PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) will add the -I ${CMAKE_CURRENT_SOURCE_DIR} to your Swift invocation, which will see that modulemap.

Then you should be able to import CppInterop from your Swift file to import the declarations in the C++ header. There are examples of this in the standard library:

The SwiftShims module is defined here swift/stdlib/public/SwiftShims/swift/shims/module.modulemap at a69dbb3366e64d835378e975e3963790e7e4f930 · swiftlang/swift · GitHub

The main difference is that you want it in C++ mode, which is where the requires cplusplus comes into the picture.

It's all about translation units. C/C++ translation units are a single C/C++ file that turns into a single object file. #include is effectively like taking the header file and copy-pasting the contents into the C/C++ file, so all of the header context is available in that one translation unit. Some C/C++ compiler drivers allow you to pass multiple C/C++ files to the compiler invocation, but the driver breaks the invocation apart into separate frontend invocations that independently compile everything and emit object files separately. CMake always invokes the C/C++ compiler separately for each object file. Swift translation units are all of the source files in a given module, so it needs more context. CMake 3.29 and after also separate the object file emission into a separate invocation. Finally once all of the objects are emitted, some compiler is invoked as a linker driver, taking all of the objects and either archiving them into a static archive, or linking them into a dynamic library/executable. Which compiler that is decided by the LINKER_LANGUAGE target property.

1 Like

It didn't work for me. Does it work for you? I even added import CppInterop, but it does not really matter because of this warning:

warning: file 'main.swift' is part of module 'CppInterop'; ignoring import
1 | import CppInterop
  |        `- warning: file 'main.swift' is part of module 'CppInterop'; ignoring import
2 |
3 | let a = add_numbers(1, 2)

Try adding @_exported to the import statement (@_exported import CppInterop). It's an executable so you aren't really re-exporting the interface, but that should hint to the compiler that it should be looking for a clang module, not a Swift module while compiling the file.

It worked. It compiles successfully now. Although, the editor does not understand what we did there and it keeps complaining about the import.

Just to confirm a few things, you're using the Ninja generator and the editor is using sourcekit-lsp to provide language info? If so, you should be able to pass -DCMAKE_EXPORT_COMPILE_COMMANDS=YES to your CMake invocation to generate the compile-commands (can just run cmake . -DCMAKE_EXPORT_COMPILE_COMMANDS=YES to avoid needing to reconfigure and recompile everything), and then go to the root of your project and create a symlink to the compile_commands.json file that was generated into your build directory.
Then I recommend restarting your editor to make sure that sourcekit-lsp is seeing the compile commands.

To be honest, and I am not sure why, doing this (having compile commands at the root of the project) just crashes the source kit somehow…