Can I mix C++ and Swift in the same swift package, using the Swift Package Manager?

Viewed 175

I would like to write one module in C++, and make some functions in C that are accessible in Swift.

I am somewhat baffled, because no matter what I do, the SPM insists on attempting to compile the C++ code as if it were Objective-C, and of course it cannot find the right headers.

Here is my attempt.

Source directory structure:

Sources
|
+-CxxModule
| |
| +-include
| | |
| | +-CxxModule.hpp
| |
| +-CxxModule.cpp
|
+-SwiftModule
  |
  +-SwiftModule.swift

The manifest Package.swift is as follows:

// swift-tools-version: 5.6

import PackageDescription

let package = Package(
    name: "CxxLibrary",
    products: [
        .library(
            name: "CxxLibrary",
            targets: ["SwiftModule"]),
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "CxxModule",
            dependencies: []),
        .target(
            name: "SwiftModule",
            dependencies: ["CxxModule"],
            path: "Sources/SwiftModule"
        ),
    ]
)

CxxModule.hpp is as follows:

#ifndef CxxModule_hpp
#define CxxModule_hpp

#include <iostream>

extern "C" void printHello();

#endif /* CxxModule_hpp */

CxxModule.cpp is as follows:

#include "CxxModule.hpp"

void printHello() {
    // use something from the standard library to make sure
    // c++ is really being used
    std::cout << "Hello, world!" << std::endl;
}

Finally, SwiftModule.swift:

import CxxModule

What am I missing? Is there a way to tell SPM that the module is supposed to be in C++? Or is this simply unsupported at the moment?

Note: the C++ compiles just fine if I eliminate the Swift target.

2 Answers

I was able to resolve the problem. The answer is “Yes” so long as the header exposed to Swift is readable in pure C.

First, I moved all C++-specific code (especially headers referencing the standard library) into the source cpp file:

#include <iostream>
#include "CxxModule.hpp"

void printHello() {
    // use something from the standard library to make sure
    // c++ is really being used
    std::cout << "Hello, world!" << std::endl;
}

Second, I added macros to make the header readable in both C and C++:

#ifndef CxxModule_hpp
#define CxxModule_hpp

#ifdef __cplusplus
extern "C" {
#endif

void printHello();

#ifdef __cplusplus
}
#endif

#endif /* CxxModule_hpp */

The file structure and Swift module remain unchanged.

The lesson: any header that is going to be read by both C++ and Swift must be readable by both. (Swift is able to understand C headers, but not C++ headers, at least in its present state.)

A couple of notes for other people struggling with C and Swift (for C++ is "simply" a matter of naming.. I do know, but without a functioning C code, C++ is mission impossible..)

to recap:

  1. suppose You have "C" code, let call it generically libC (a folder), that contains both *.c and *.h files.

  2. You want to build a wrapper, say libCWrapper

  3. build a package in Xcode names libCWrapper

  4. Xcode should build a Package.swift similar to: (comments removed)

    import PackageDescription

    let package = Package( name: "libCWrapper", products: [ .library( name: "libCWrapper", targets: ["libCWrapper"]), ], targets: [ .target( name: "libCWrapper", dependencies: []), .testTarget( name: "libCWrapperTests", dependencies: ["libCWrapper"]), ] )

  5. now first trick: we need TWO targets and TWO products.. so add:

...

 products: [
        .library( name: "libCWrapper", targets: ["libCWrapper"]),
        .library( name: "libC", targets: ["libC"]),
    ],

..

  .target(
        name: "libCWrapper",
        dependencies: ["libC"]),
    
    .target(
            name: "libC",
            dependencies: []),

Note: swift libCWrapper depends ON libC.

  1. we must also Add/change layout of "C sources:

all "C" code must go inside a fodder names as "C" target, but include one level UP. So:

Sources --->[libC] sampleCall.c [include] ---> [sampleCall.h]

so:

enter image description here

  1. pls add also targets, I f You need to make some tests:

    platforms: [ .macOS(.v10_12), .iOS(.v9)],

  2. inside tests:

@testable import libCWrapper @testable import libC

Add the C product, TOO. so tests can be:

import XCTest
@testable import libCWrapper
@testable import libC

final class libCWrapperTests: XCTestCase {
    
    func testExample() throws {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct
        // results.
        XCTAssertEqual(libCWrapper().text, "Hello, World!")
    }
    
    func testGimmeFive(){
        let n = gimmeFive()
        XCTAssertEqual(n, 5)

    }
}

where in C we have:

//
//  sampleCall.c.c
//  
//
//  Created by ing.conti on 24/08/22.
//

#include "sampleCall.h"


int gimmeFive(void){
    return 5;
}

now, as You can call C from swift in test, is only a matter of "open" / "public" and similar.

Related