4

Summary: Problems with C++ header imports when creating a Swift package containing a C++ module and an Obj-C module.

There is no Swift code involved in the modules or the "clients" here - this is just about using Swift Package Manager to create and use the modules. I have often used C++ code from Objective-C clients (and from Objective-C wrappers used by Swift code) in the past using frameworks.

I am trying to use the Swift Package Manager to create a package that contains both C++ and Objective-C APIs. I have a Swift package, with working unit tests, structured as follows: MyPackageCPP is a C++ implementation; MyPackage is an objective-C wrapper around that.

The Package.swift file for this is

import PackageDescription

let package = Package(
    name: "MyPackage",
    platforms: [.iOS(.v13)],
    products: [
        .library(name: "MyPackageCPP", targets: ["MyPackageCPP"]),
        .library(name: "MyPackage", targets: ["MyPackage"]),
    ],
    targets: [
        .target(
            name: "MyPackageCPP",
            path: "Sources/MyPackageCPP",
            publicHeadersPath: "include"
        ),
        .target(
            name: "MyPackage",
            dependencies: ["MyPackageCPP"],
            path: "Sources/MyPackage",
            exclude: [
                "Info.plist",
            ],
            publicHeadersPath: "include"
        ),
        .testTarget( // C++ tests
            name: "MyPackageCPPTests",
            dependencies: ["MyPackageCPP"]
        ),
        .testTarget( // Obj-C wrapper tests
            name: "MyPackageTests",
            dependencies: ["MyPackage"]
        )
    ],
    cxxLanguageStandard: .gnucxx1z
)

And the directory structure is

MyPackage
 |
 .- Sources
 |  |
 |  .- MyPackage
 |  |  |
 |  .  .- Classes (implementation files for Objective-C wrappers)
 |  |  |
 |  |  .- include
 |  |        MyClass.h (Obj-C header)
 |  |
 |  .- MyPackageCPP
 |     |
 |     .- Classes (C++ Implementation files)
 |     |
 |     .- include
 |           MyClassCPP.h (C++ header)
 |           MyPointClass.h (Simple C-header defining a custom struct)
 |
 .- Tests
    |
    .- MyPackageCPPTests
    |     MyPackageCPPTests.mm (XCTestCase-based unit tests for C++ API)
    |
    .- MyPackageTests
          MyPackageTests.mm (XCTestCase-based unit tests for pure Obj-C API)

The key header files:

MyPointClass.h (meant to define a simple C type usable by both the C++ class and its Obj-C wrapper):

struct MyPoint {
    double x;
    double y;
};

MyClassCPP.h:

#pragma once

#include "MyPointClass.h"

#include <string>
#include <memory>

class MyClassCPP {

public:
    MyClassCPP() = delete;
    MyClassCPP(const std::string &info);
    ~MyClassCPP();

    bool initialized();
    MyPoint modifyPoint(MyPoint point);


private:
    struct Impl;
    std::unique_ptr <struct Impl> _pImpl;
};

MyClass.h (Obj-C wrapper class)

#import <UIKit/UIKit.h>

#import "MyPointClass.h"

NS_ASSUME_NONNULL_BEGIN

// iOS Platform specific implementation

@interface MyClass : NSObject

-(instancetype)initWithInfoInAString:(NSString*)someInfoInAString;

-(MyPoint)modifyPoint:(MyPoint)point;

@property (atomic, readonly) BOOL initialized;

@end

NS_ASSUME_NONNULL_END

The problem

I've been having problems with the includes. The unit tests work as they are now, but if I change the Obj-C test file from MyPackageTests.mm to MyPackageTests.m, I get a build failure 'string' not found. I understand that Objective-C++ (*.mm) is needed to correctly build objective-C files that use C++, but the direct include chain for MyPackageTests does not include any C++. Rather, it appears from the error that, when building module the Obj-C MyPackage, all of the header files from the C++ package MyPackageCPP are included, even though only the C header MyPointClass.h is explicitly included by the Obj-C MyPackage (via the #import "MyClass.h") (The clue being "In file included from <module-includes>")

I get the same build error when I try to include the Obj-C module into a simple Objective-C test app MyApp that imports the Obj-C wrapper MyPackage:

While building module 'MyPackage' imported from /Users/{user}/TestCode/MyPackage/Tests/MyPackageTests/MyPackageTests.m:3:
While building module 'MyPackageCPP' imported from /Users/{user}/TestCode/MyPackage/Sources/MyPackage/include/MyClass.h:3:
In file included from <module-includes>:1: /Users/{user}/TestCode/MyPackage/Sources/MyPackageCPP/include/MyClassCPP.h:5:10: fatal error: 'string' file not found
#include <string>

The question

Are all of the headers in the C++ package necessarily included in the Obj-C wrapper build? (I have tried adding MyClassCPP.h to the excludes for target MyPackage, to no avail.) Or is there a way around this that will allow me to include only the pure C-headers from the C++ package, when building the Obj-C wrapper MyPackage (while keeping the C++header available for C++ clients)?

bphi
  • 1,015
  • 6
  • 16
  • For Swift, your Objective-C headers will have to import only either C or Objective-C headers, and not C++ headers. – Eljay Jun 15 '21 at 12:42
  • Understood. I'm not trying to use either the Obj-C or the C++ modules in Swift here, and the single Obj-C header (explicitly) imports only the pure C header ```MyPointClass.h```. The Obj-C header does not import the C++ header ```MyClassCPP.h``` (either directly, or indirectly via the ```MyPointClass.h``` import) – bphi Jun 15 '21 at 13:02
  • You may find [Can I mix Swift with C++?](https://stackoverflow.com/questions/24042774/can-i-mix-swift-with-c-like-the-objective-c-mm-files) useful, and possibly [Obj-C & Swift interop](https://rderik.com/blog/understanding-objective-c-and-swift-interoperability/). My own project will likely experience the same issues in the near future. – Eljay Jun 15 '21 at 13:29
  • @Eljay thanks - I understand the issues in those links, and I've created *frameworks* for using C++ code within Obj-C (and Swift), with no problems as long as I hid all C++ from pure Obj-C callers or Swift callers. It seems like the issue here is about how Swift Package Manager handles the imports: it looks as though it imports everything (error is "```In file included from ```" rather than from a single, specific header.) If my understanding is correct, then it seems a solution would involve somehow excluding the C++ header from the Obj-C module build. – bphi Jun 15 '21 at 15:23

1 Answers1

4

is there a way around this that will allow me to include only the pure C-headers from the C++ package, when building the Obj-C wrapper MyPackage (while keeping the C++header available for C++ clients)?

After trying numerous approaches, I found one that works. These changes were involved:

  1. Modify struct file MyPointClass.h as follows (which I renamed here to MyPackageCommonHeaders.h):

    typedef struct MyPoint {
        double x;
        double y;
    } MyPoint;
    
  2. Add file MyPackageCommonHeaders.c containing only the following include:

    #include "../include/MyPackageCommonHeaders.h"
    
  3. Create a new module just for the shared headers

    MyPackage
     |
     .- Sources
     |  |
     |  .- MyPackage
     |  |  |
     |  |  .- Classes (implementation files for Objective-C wrappers)
     |  |  |
     |  |  .- include
     |  |        MyClass.h (Obj-C header)
     |  |
     |  .- MyPackageCommonHeaders  // NEW MODULE
     |  |  |
     |  |  .- Classes (C++ Implementation files)
     |  |        MyPackageCommonHeaders.c // NEW FILE
     |  |  |
     |  |  .- include
     |  |        MyPackageCommonHeaders.h (Simple C-header)
     |  |
     |  .- MyPackageCPP
     |     |
     |     .- Classes (C++ Implementation files)
     |     |
     |     .- include
     |               MyClassCPP.h (C++ header)
     |
     .- Tests
        |
        .- MyPackageCPPTests
        |     MyPackageCPPTests.mm (XCTestCase-based unit tests for C++ API)
        |
        .- MyPackageTests
              MyPackageTests.m (XCTestCase-based unit tests for pure Obj-C API)
    

(Notice that MyPackageTests.m is no longer Obj-C++, which was my orginal goal)

  1. Modify Package.swift to include the new module, as list it as a dependency for the C++ and Obj-C API mmodules:

    import PackageDescription
    
    let package = Package(
        name: "MyPackage",
        platforms: [.iOS(.v13)],
        products: [
            .library(name: "MyPackageCommonHeaders", targets: ["MyPackageCommonHeaders"]),
            .library(name: "MyPackageCPP", targets: ["MyPackageCPP"]),
            .library(name: "MyPackage", targets: ["MyPackage"]),
        ],
        targets: [
            .target(
                name: "MyPackageCPP",
                dependencies: ["MyPackageCommonHeaders"],
                path: "Sources/MyPackageCPP",
                publicHeadersPath: "include"
            ),
            .target(
                name: "MyPackage",
                dependencies: ["MyPackageCPP", "MyPackageCommonHeaders"],
                path: "Sources/MyPackage",
                exclude: [
                    "Info.plist",
                ],
                publicHeadersPath: "include"
            ),
            .testTarget( // C++ tests
                name: "MyPackageCPPTests",
                dependencies: ["MyPackageCPP"]
            ),
            .testTarget( // Obj-C wrapper tests
                name: "MyPackageTests",
                dependencies: ["MyPackage"]
            )
        ],
        cxxLanguageStandard: .gnucxx1z
    )
    

With this, I am able to create an pacakage that supports a C++ API, and an pure Obj-C wrapper API that uses the C++ code. Note that the Obj-C wrapper does not force promotion of Obj-C callers to Obj-C++. A key part of this is that a simple C-struct (MyPoint) is available to both APIs, so that the the Obj-C wrapper doesn't have to translate; i.e., the Obj-C wrapper can return the same object it gets from its wrapped C++ methods.

bphi
  • 1,015
  • 6
  • 16