Problem including a C Library with Swift executable using SwiftPM

I am having a really frustrating time trying to get a non-system C library to work with the SPM. I have tried everything including talking to ChatGPT 3.5 to help me out. Everything I have looked at on the internet has led me to feeling that I am doing it correctly but it just doesn't work. So here it is:

I have a Clibrary called "prism". This library comes in both a static (libprism.a) and shared version (lib prism.dylib). These are not installed via a package manager on the system. These libraries should be included in the project.

Here is my project structure:

.
β”œβ”€β”€ Package.swift
└── Sources
β”œβ”€β”€ libprism
β”‚ β”œβ”€β”€ includes
β”‚ β”‚ β”œβ”€β”€ prism
β”‚ β”‚ β”‚ β”œβ”€β”€ ast.h
β”‚ β”‚ β”‚ β”œβ”€β”€ defines.h
β”‚ β”‚ β”‚ β”œβ”€β”€ diagnostic.h
β”‚ β”‚ β”‚ β”œβ”€β”€ encoding.h
β”‚ β”‚ β”‚ β”œβ”€β”€ node.h
β”‚ β”‚ β”‚ β”œβ”€β”€ options.h
β”‚ β”‚ β”‚ β”œβ”€β”€ pack.h
β”‚ β”‚ β”‚ β”œβ”€β”€ parser.h
β”‚ β”‚ β”‚ β”œβ”€β”€ prettyprint.h
β”‚ β”‚ β”‚ β”œβ”€β”€ regexp.h
β”‚ β”‚ β”‚ β”œβ”€β”€ static_literals.h
β”‚ β”‚ β”‚ β”œβ”€β”€ util
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_buffer.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_char.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_constant_pool.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_integer.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_list.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_memchr.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_newline_list.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_state_stack.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_string.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_string_list.h
β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ pm_strncasecmp.h
β”‚ β”‚ β”‚ β”‚ └── pm_strpbrk.h
β”‚ β”‚ β”‚ └── version.h
β”‚ β”‚ └── prism.h
β”‚ β”œβ”€β”€ libprism.a
β”‚ β”œβ”€β”€ libprism.dylib
β”‚ └── module.modulemap
└── test
└── main.swift

7 directories, 30 files

Here is the contents of the module.modulemap file:

module libprism {
    header "includes/prism.h"
    link "prism"
    export *
}

Here is my Package.swift file:

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

import PackageDescription

let package = Package(
    name: "test",
    products: [
        .executable(name: "test", targets: ["test"])
    ],
    targets: [

        .systemLibrary(
            name: "libprism",
            path: "Sources/libprism"
        ),

        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "test",
            dependencies:["libprism"]
),
    ]
)

Here is the output of the swift build command:

❯ swift build        
Building for debugging...
error: emit-module command failed with exit code 1 (use -v to see invocation)
<module-includes>:1:9: note: in file included from <module-includes>:1:
#import "includes/prism.h"
        ^
/Users/bruce/Developer/swift/test/Sources/libprism/includes/prism.h:10:10: note: in file included from /Users/bruce/Developer/swift/test/Sources/libprism/includes/prism.h:10:
#include "prism/util/pm_buffer.h"
         ^
/Users/bruce/Developer/swift/test/Sources/libprism/includes/prism/util/pm_buffer.h:9:10: error: 'prism/defines.h' file not found
#include "prism/defines.h"
         ^
/Users/bruce/Developer/swift/test/Sources/test/main.swift:1:8: error: could not build Objective-C module 'libprism'
import libprism
       ^
<module-includes>:1:9: note: in file included from <module-includes>:1:
#import "includes/prism.h"
        ^
/Users/bruce/Developer/swift/test/Sources/libprism/includes/prism.h:10:10: note: in file included from /Users/bruce/Developer/swift/test/Sources/libprism/includes/prism.h:10:
#include "prism/util/pm_buffer.h"
         ^
/Users/bruce/Developer/swift/test/Sources/libprism/includes/prism/util/pm_buffer.h:9:10: error: 'prism/defines.h' file not found
#include "prism/defines.h"
         ^
/Users/bruce/Developer/swift/test/Sources/test/main.swift:1:8: error: could not build Objective-C module 'libprism'
import libprism
       ^
error: fatalError

I have been trying to get this to compile and link for hours. I have tried different combinations of things with no avail. Any help would be appreciated.

Thanks

Does it help if you change includes to include? (Just a guess.)

Why are you using the .systemLibrary target? That would be useful for libraries and include files installed system wide - that is somewhere in /usr or /usr/local. In your case where the lib is part of the package, you probably want just the .library target.

Do you have that code somewhere on GitHub? Push it out and I will take a look. FYI I do have many Swift wrappers around C libraries written like this and it works nicely... so it should be fairly easy to get it working...

1 Like

Here is the repo: GitHub - brucemontegani/test.

I have been able to build it.

❯ swift build 
Building for debugging...
[3/3] Linking test
Build complete! (2.93s)

However, when I look at the binary test, libprism is not linked into it. I used otool -L test to find out:

~/Developer/swift/test/.build/debug main +32 !4 ?2
❯ otool -L test
test:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.61.1)
	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1600.157.0)
	/usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 5.9.2)

I also cannot import the prism library into the main.swift file.

Thank you for your help.

I was able to compile, link and run with the following changes:

  • In headers, change all paths to prism folder as relative path ex:
/**
 * @file ast.h
 *
 * The abstract syntax tree.
 */
#ifndef PRISM_AST_H
#define PRISM_AST_H

#include "../prism/defines.h"
#include "../prism/util/pm_constant_pool.h"
#include "../prism/util/pm_integer.h"
#include "../prism/util/pm_string.h"
/**
 * @file pm_buffer.h
 *
 * A wrapper around a contiguous block of allocated memory.
 */
#ifndef PRISM_BUFFER_H
#define PRISM_BUFFER_H

#include "../../prism/defines.h"
#include "../../prism/util/pm_char.h"

module.modulemap

module libprism {
    header "include/prism.h"
    //link "prism"
    export *
}

Package.swift

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

import PackageDescription

let package = Package(
    name: "test",
    products: [
        .executable(name: "test", targets: ["test"])
    ],
    targets: [

        .systemLibrary(
            name: "libprism",
            path: "Sources/libprism"
        ),

        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "test",
            dependencies:["libprism"],
            linkerSettings: [.unsafeFlags(["-L/usr/local/lib", "-lprism", "-rpath", "/usr/local/lib"])]
),
    ]
)

Move the libprism.dylib to /usr/local/lib

From /usr/local/lib change add the dylib path:

% install_name_tool -id "@rpath/libprism.dylib" libprism.dylib
% install_name_tool -add_rpath "/usr/local/lib/" libprism.dylib

main.swift

import Foundation
import libprism

print("Hello World!")
print("prism version \(PRISM_VERSION_MAJOR).\(PRISM_VERSION_MINOR).\(PRISM_VERSION_PATCH)")


Build and run...

Hello World!
prism version 0.24.0
Program ended with exit code: 0

2 Likes

I read-up on using C libraries in SPM in an attempt to learn more and help answer this question.

As far as I can see these are the following options: (please do correct me if I'm wrong.)

  • Get SPM to compile the library from source and make that a 'Library Target' (not suitable in this case)
  • Wrap the compiled library into an XCFramework and use that as a 'Binary Target' - This is only available on Apple Platforms.
  • Move the compiled library out of the package and use it as a 'System Library Target' - requires installation of the library separately from the package and therefore is fragile across devices.

Is it possible to encapsulate the already compiled library inside the package (as the OP was attempting and @mman suggested above)?

Thank you for this solution. From what I can gather you cannot put the static or shared library in the project directly. It must be installed/moved into a system directory.

I am not sure about the usage of the install_name_tool. I have never used this before.

What if I what to statically link the libprism.a library?

Again, thanks for the help. I hope in future release of SPM we can just use the binary in the project.

1 Like

You can link to the libprism.a static lib instead using the same linkerSetting unsafe flag. Just put the path to libprism.a.

            linkerSettings: [.unsafeFlags(["-L/usr/local/lib", "-lprism.a"])]  // static link - here libprism.a is located in /usr/local/lib
            //linkerSettings: [.unsafeFlags(["-L/usr/local/lib", "-lprism", "-rpath", "/usr/local/lib"])] // dynamic link

When using linking to dylib, it is necessary that the executable know where to load the library, and, for the library the install name well setup. For example when you build a dylib with Xcode, there is an entry in build setting called "Dynamic Library Install Name" where you specify the dylib install path. You can specify a fixed path like /usr/local/lib or a relative path, for example if you want the dylib to reside in a lib folder relative to your executable.

I suggest this reading about rpath and associated tools: