What about some fancier C++ in with the Swift?
This article is part of a series.
The CMake “Hello World” prime number provider code seemed like a fun thing to try to bring into this new Swift/C++ Package. The file loading requirement made it a tricky choice, but it provided good practice importing C++ classes into Swift.
Related Repo
References
- https://developer.apple.com/videos/play/wwdc2023/10172/
- https://www.swift.org/documentation/cxx-interop/
Templates
The random-number generator that creates the index number used to go fetch a prime number from the file uses C++ templates. Swift can interact with those a little bit, but not perfectly. A post on the forums provide a good break down of the issues:
It’s not a big barrier to get this example working since the call to fancy_random
will be internal to its class in the end (see below), but I tired it out anyway.
A reminder of the C++:
template <typename T> T fancy_random(const T &min, const T &max) {
std::random_device seed_maker;
std::mt19937 generator(seed_maker()); // Random-number engine to use (Mersenne-Twister)
std::uniform_int_distribution<T> distribution(min,
max); // Guaranteed unbiased
return distribution(generator);
}
A successful swift call:
public func randomInt(_ min:CUnsignedChar, _ max:CUnsignedChar) -> CUnsignedChar {
fancy_random(min, max)
}
Unsuccessful Swift calls. (causes linker errors)
public func randomNumber(_ min:UInt8, _ max:UInt8) -> UInt8 {
fancy_random(min, max)
}
public func randomNumber(_ min:CUnsignedInt, _ max:CUnsignedInt) -> CUnsignedInt {
fancy_random(min, max)
}
Bug report filed: https://github.com/apple/swift/issues/67901
Accessing the Bundle
In order to test moving files around with CMake, I designed the prime number library code to load and scan a file with prime numbers in it. Packages with resource files need to access those resources via a Bundle type.
There MAY be a way to massage the C++ to import either NSBundle
or the Swift type Bundle
, but it’s way above my current pay grade. I haven’t even been able to successfully get my hands on swift::String
yet.
- https://github.com/apple/swift-package-manager/blob/48192bbdd2800a38a82fa658a244e70b3167ec5d/Sources/Build/BuildDescription/ClangTargetBuildDescription.swift#L304-L364
- https://forums.swift.org/t/access-a-package-s-bundle-module-from-c-c/63339
A Bundle
related side bar: It looks like the Swift 5.9 Package Manager Tools now has an automated .embedInCode option? It looks the accessor is a dictionary? I wonder how that will work from the C++? To. Do. Very handy, even just for Swift.
Creating a placeholder class in C++ called BundleManger
provided an immediate work around for not being able to get my hands on Bundle
. A BundleManger
gets initialized with a path string generated by the Swift target’s Bundle
reference. In this case, the Swift library was given a mock resource file so Bundle.module
would exist. This code relies on the Swift target’s and the C++ target’s resource bundles having the same root path. Very fragile.
It’s my understanding from the WWDC video that BundleManager
should be passed into Swift as a value type by default, even though it’s a class. I did not confirm it.
ThingCatalog.swift
import Foundation //for Bundle
import cxxLibrary // for my class
public struct ThingCatalog {
var bundleManager:BundleManager //SO! EASY!!!
public init() {
let strippedPath = String(URL(filePath: Bundle.module.bundlePath)
.deletingLastPathComponent()
.path())
self.bundleManager = BundleManager(
std.string(strippedPath),
"CxxInteropLibrary_cxxLibrary.bundle"
)
}
//...
}
In the call to initialize a BundleManager
note the std.string()
casting of a Swift string into the C++ standard library string the initializer requires. The ability to draw upon C++ standard library types gets enabled in the package when swiftSettings: [.interoperabilityMode(.Cxx)]),
gets added to the target’s definition in Package.swift
. In order to refer to stdlib
types one must import at least one of the following
- a C++ product that imports the desired standard library types in its own headers
CxxStdlib
- Bug report: compiler warning but not failure if no companion lib imported?Foundation
- it’s bad practice to go importing Foundation willy nilly. However, if it’s needed anyway,CxxStdLib
appears to come for the ride IF the projects build setting is C++/Objective C++ mode. This hold true for XCode projects updated via the GUI as well.
The “overlay implementations” are still being actively worked on. String actually seems the most fleshed out.
- https://github.com/apple/swift/tree/main/stdlib/public/Cxx
- https://duckduckgo.com/?q=overlay+implementation+GSOC+forums.swift.org
BundleManager.hpp
#ifndef BundleManager_hpp
#define BundleManager_hpp
#include <stdio.h>
#include <string>
#include <swift/bridging> //allows for annotating
class BundleManager {
public:
BundleManager(std::string path, std::string name) {
bundle_path = path;
this_bundle_name = name;
}
std::string path() const SWIFT_COMPUTED_PROPERTY {
return bundle_path;
}
std::string name() const SWIFT_COMPUTED_PROPERTY {
return this_bundle_name;
}
std::string path_for_resource(std::string name, std::string extension);
private:
std::string bundle_path;
std::string this_bundle_name;
};
#endif /* BundleManager_hpp */
BundleManager.cpp
std::string BundleManager::path_for_resource(std::string name, std::string extension) {
return bundle_path + this_bundle_name + "/" + name + "." + extension;
}
This whole approach worked to get a valid path into C++, but without additional structure every prime number retrieving function call would of had to collect that information as a parameter. Yuck.
Making PrimeNumberGenerator class
Instead of rewriting all the functions, I made a PrimeNumberGenerator
object that holds a BundleManager
privately.
I could have made PrimeNumberGenerator
a subclass of BundleManager
, but the nuances of how the many shades of class inheritance in C++ maps into Swift requires more study on my part. I’m just going to avoid that for the moment.
- https://github.com/apple/swift/issues/66323
- https://forums.swift.org/t/swift-c-interop-and-polymorphism/66103
I did, however, want to try out making an explicit reference type.
PrimeNumberGenerator.hpp
#ifndef PrimeNumberGenerator_hpp
#define PrimeNumberGenerator_hpp
#include <stdio.h>
#include <string>
#include <swift/bridging> //allows for annotating
class BundleManager {
//this lives here now
};
class PrimeNumberGenerator {
public:
PrimeNumberGenerator(std::string bundle_path)
:bundle(bundle_path, "CxxInteropLibrary_cxxLibrary.bundle"),
max_line_length(16), // Max line is 12 characters long
max_prime_length(6) //// 10,000th prime is 104729
{
}
//prime in first 10000 primes at index
int prime_at(int index);
//random prime within first 10000 primes
int random_prime();
private:
//All for file loading.
BundleManager bundle;
const int max_line_length;
const int max_prime_length;
int readNthLine(std::ifstream &in, int index);
template <typename T> T fancy_random(const T &min, const T &max);
unsigned char random_uint8(const unsigned char &min, const unsigned char &max);
}; SWIFT_SHARED_REFERENCE(pngRetain,pngRelease);
//At the bottom or top? Seen both. Which style is preferred?
void pngRetain(PrimeNumberGenerator *);
void pngRelease(PrimeNumberGenerator *);
#endif /* PrimeNumberGenerator_hpp */
PrimeNumberGenerator.cpp
This will look pretty similar to PrimeLib.cpp, just with class namespace prefixes.
//
// PrimeNumberGenerator.cpp
//
//
// Created by Carlyn Maw on 8/13/23.
//
#include "PrimeNumberGenerator.hpp"
#include <fstream>
#include <random>
#include <string>
//Same as before.
std::string BundleManager::path_for_resource(std::string name, std::string extension) {
return bundle_path + this_bundle_name + "/" + name + "." + extension;
}
int PrimeNumberGenerator::readNthLine(std::ifstream &in, int index) {
for (int i = 0; i < index - 1; ++i) {
in.ignore(max_line_length, '\n');
}
std::string s;
s.reserve(max_prime_length);
std::getline(in.ignore(max_line_length, ' '), s);
return std::stoi(s);
}
uint8_t PrimeNumberGenerator::random_uint8(const uint8_t &min, const uint8_t &max) {
return fancy_random(min, max);
}
template <typename T> T PrimeNumberGenerator::fancy_random(const T &min, const T &max) {
std::random_device seed_maker;
std::mt19937 generator(
seed_maker()); // Random-number engine to use (Mersenne-Twister)
std::uniform_int_distribution<T> distribution(min,
max); // Guaranteed unbiased
return distribution(generator);
}
// random prime within first 10000 primes
int PrimeNumberGenerator::random_prime() { return prime_at(fancy_random(0, 10000)); }
// prime in first 10000 primes at index
int PrimeNumberGenerator::prime_at(int index) {
//assumes package uses .process for resources
std::ifstream primesFile(bundle.path_for_resource("b000040", "txt"));
if (primesFile.is_open()) {
return readNthLine(primesFile, index);
} else {
// handle error
return -1;
}
}
Add the Test, and run
import XCTest
@testable import InteropLibrary
final class CxxInteropLibraryTests: XCTestCase {
//...
//MARK: PrimeNumberGenerator Related
func testChosenPrime() throws {
var things = ThingCatalog()
XCTAssertEqual(things.nthPrime(n: 998), 7901);
}
//...
}
Once that’s in, a quick pop over to Terminal and a swift test
shows that it all works! Whew!