Using a dynamically linked C library (.dylib) from SPM

I have compiled some C code and built a .dylib. I also have a C header for this .dylib.

I want to create a target for it in Swift Package Manager. I would then like to use that target as a dependency in a Swift only (source) package, so I can write/test a nicer way to do the C interop in Swift.

There are a lot of examples for system libraries, but I haven't found a single one for using a .dylib/C Header that I've created. Or they all are based on having the C source. I've searched for hours and am close to just giving up and opening a TSI.

If an example isn't available, answers to the following will help me move forward I think:

  • Does SPM support bridging headers?
  • Are system libraries the only way to use a .dylib?
  • Does SPM support embedding a .dylib this way?
  • To embed a .dylib, do I just add it as a .copy resource in the "C" package?
  • Do I need to add a cSettings flag for the dylib?
  • Do I need a module map?

I should note I can't just drop the source in (of which there are some examples for) because this is actually a Ziglang project, that I am compiling to a .dylib and using the C ABI. I was able to use my .dylib and C header in a test macOS app, but can't seem to figure out how to access the code using SPM. So I don't think it's a problem with my Zig code.

I'm not a C programmer, so please forgive any incorrect terms/ideas. I'd love to make this work!

Thanks!

2 Likes

Here's what I currently have in my Package.Swift file (ZigTools is the Swift Source package, ZigSyntaxChecker is where I'm trying to link the dylib and use the header file):

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "ZigTools",
  products: [
    .library(
      name: "ZigTools",
      targets: ["ZigSyntaxChecker", "ZigTools"]),
  ],
  dependencies: [],
  targets: [
    .target(
      name: "ZigTools",
      dependencies: ["ZigSyntaxChecker"],
      path: "Sources/ZigTools")
    ,
    .target(
      name: "ZigSyntaxChecker",
      dependencies: [],
      path: "Sources/ZigSyntaxChecker",
      exclude: [
        "src/",
        "README.md",
        "build.zig"
      ],
      resources: [
        Resource.copy("libZigSyntaxChecker.dylib"),
      ],
      publicHeadersPath: "include",
      cSettings: [
        .headerSearchPath("include/ZigSyntaxChecker.h")
      ]
    ),
    .testTarget(
      name: "ZigToolsTests",
      dependencies: ["ZigTools"]
    ),
  ]
)

My package file tree:

This currently gives the following error on the ZigToolsTests target:

Build input file cannot be found: 
'/Users/rudedogg/Library/Developer/Xcode/DerivedData/ZigTools-.../Build/Products/Debug/ZigSyntaxChecker.o'.

I assume this is just a sign of larger issues in my Package.swift configuration.

1 Like

Hello @rudedogg, did you find a solution to resolve your issue ? i'm in the same case instead i use c++ dylib.

Sorry, I did not :frowning: .

On Apple platforms you'll want to wrap the dylib in an xcframework and add a binaryTarget to the package. I've opened a PR to show the basics.

1 Like

Amazing! I commented on GitHub but thanks again!

If someone else comes along and wants to look at the code it's here:

I'm looking for something very similar to your scenario @rudedogg ("C code and built a .dylib. I also have a C header for this .dylib.").

Would it be possible for you to document the steps you took in Xcode/Terminal to implement this please?

When building a Swift Package library project generating .dylib (.so on Linux or .dll on Windows) you will will find .swiftinterface and .swiftmodule files in the build directory. These two files with the .dylib can be placed somewhere and used from a SwiftPM executable project.

For example, I have build a library named MAXComKit and placed in /usr/local/lib/MAXComKit the 3 files:

libMAXComkit.dylib
MAXComKit.swiftinterface
MAXComKit.swiftmodule

In addition, if you have to link to a C library, you must add a module.modulemap file and a header folder to the 3 previously mentioned files. The module.modulemap file will look like this:

module MXFoundation {
    header "headers/MXFoundation.h"
    link "maxtmxf"
    export *

}

header with the path to the header folder containing the .h files.
link with the name of the C library to link with

And I have a Swift Package executable project like this:

// swift-tools-version: 5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

// MAXComKit module located in /usr/local/lib
let maxcomSwiftSettings  : [SwiftSetting]   = [ .unsafeFlags(["-I/usr/local/lib/MAXComKit"]) ]

// Link with MAXComKit library
let maxcomLinkerSettings : [LinkerSetting]  = [ .unsafeFlags(["-L/usr/local/lib", "-lMAXComKit"]) ]

let package = Package(
    name: "MAXComKitDemos",
    products: [
        .executable(
            name: "ClientReceiveSignals",
            targets: ["ClientReceiveSignals"])
    ],
    dependencies: [],
    targets: [
        .executableTarget(
            name: "ClientReceiveSignals",
            swiftSettings: maxcomSwiftSettings,
            linkerSettings: maxcomLinkerSettings)
    ]
)

The keys are the SwiftSetting and LinkerSetting flags.

In executable project, the main.swift begin with:

//
//  MAXCom ClientReceiveSignals
//  main.swift
//  

import Foundation
import MAXComKit

print("Hello MAXComKit Client Swift API!")

The library is imported and used.
I follow the same pattern to build Swift modules .dll on Windows and .so on Linux then access from executable project with proper SwiftSetting and LinkerSetting.

I hope this can help you.

1 Like

Amazing detail, thank you :+1: