Using C in Swift away from Xcode

Hello together,
i have a command-line project, written in Swift which is build and used on multiple platforms macOS/Unix/Windows. Actually, I am trying to import some existing stuff written in C. Using Xcode, as most of you may know, this is quite straightforward using a bridging header.

However, how can i resolve this on other platforms using cmake or the "bare" swift compiler?
Is there an option to simply use the bridging.header generated by Xcode?

I've created the following simple example creating a simple struct in C to be used within Swift:

cct.c

//
//  cct.c
//  sctest
//

#include "cct.h"

cct.h

//
//  cct.h
//  sctest
//

#ifndef cct_h
#define cct_h

#include <stdio.h>

#endif /* cct_h */

struct astruct {
    int a;
};

sctest-Bridging-Header.h

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "cct.h"

main.swift

//
//  main.swift
//  sctest
//

import Foundation


var astr=astruct()

astr.a = 1

print(astr)

Best,
Bastian

I'd open Xcode's build log, see what it does and do the same:

... -import-objc-header path-to-bridging-header.h

2 Likes

I don't understand why the bridging header is needed here. A bridging header is very specific, a C module can be used directly without a bridging header. If you are looking for examples of that ...

SwiftWinRT/Sources/CWinRT at main · compnerd/SwiftWinRT · GitHub has an example of include only support, but could add implementations as well.

1 Like

Maybe they don't have the "C module" to begin with, just a .c and .h file. From there you can go "the module way" or "the bridging header way" (in the latter case you might just use the existing ".h" file as the bridging header without having a separate bridging header). The second way looks simpler but I am not very familiar with the first way to say more.

Which is why I had included the example :slight_smile: The example if you note is just that a .c and .h file, with no other content. That is just given to SPM as a target which will then generate the necessary module interface to use in other targets.

1 Like

Well, I do need "Package.swift" file, right? Perhaps a folder structure, and some ".swiftpm" hidden stuff? Can you beat the simplicity of:

    file.c // existing
    file.h // existing, used as bridging header
    file.swift // existing

I've always used Swift Package Manager when working with C. I have my C code in a Target and import said target from a Swift Target.

More independent way may be creating your own clang module (which is afaik required by Clang Importer). As I understand it, the biggest issue is the header files in which case, modules should do the trick. The compiled C code can be linked as any other static/dynamic library.

However, I would strongly encourage you to take advantage of SPM anyway, since tools like sourcekit-lsp works great with C targets in SPM. Even if you're not able to use SPM for any reason, you may create a small project in SPM and dump verbose log which will tell you what arguments in which order are passed to swiftc and ld. I do that a lot when I have trouble building something.

1 Like

Yes, a Package.swift is needed. I'm not sure what example you mean by the folder structure, though. There is no hidden .swiftpm stuff here, the 2 files and the Package.swift is all. If you are using a different build system, sure the Package.swift is replaced with a module.modulemap, but that is still trivial to create.

I would say that the SPM approach actually is simpler than the bridging header generation and use.

This is the documentation from the Swift Package Manager on how to include C modules in a SwiftPM project swift-package-manager/Usage.md at main · apple/swift-package-manager · GitHub. You will have to separate your C code into a separate target for this to work.

Not related to the question, but your #endif should be at the end of cct.h The idea is to stop duplications of all your definitions, not just duplicate includes.

2 Likes

Ouch, I could have thought of that myself. Seems like i did not see the forest for the trees while searching through the one million xcode menus to find this flag.
This was exactly what i was looking for, thank you very much!

In general, thank yiu all for the discussion.
While I find the module approach very interesting, it is exactly as tera described. We basically have the c and the h files from a larger project, which we would like to integrate into swift with as little effort as possible. But i will have definitely a look on it.

Thanks for the note, this happened in a hurry when creating the example. ;)

1 Like

I'd feel exactly the same when someone familiar with both Xcode and Android Studio (or Visual studio) tells me that their environment is easier to use :slight_smile:. It's definitely not simpler for me at this point. Let's compare.

Bridging header (from an experienced user point of view):

  1. to use a bridging header all I need to do is passing -import-objc-header path-to-bridging-header.h to the compiler
  2. if I don't know this mantra yet I'd see what Xcode did and do the same.
  3. if I can't or don't want to use an existing header file as a bridging header I can create a new header file
  4. creating a header file from scratch is trivial as it's not different syntactically to any other header.

Modules (from a newby user point of view):

  1. no way I can create the "Package.swift" from scratch (unless I did this hundred times before and I didn't). I'd have to use some tool or grab an example one.
  2. if I use an example one the first thing is: that's not a file format I am already familiar with, so there's some learning curve.
  3. I won't immediately know what parameters to change to what values. For example I've spotted "-parse-as-library" which is unwanted for my usage, but other inexperienced users might not notice that, and I probably didn't spot yet something important and obvious to others.
  4. There are quite a few targets to figure out what's doing what, and what name goes where.
  5. Perhaps the target name should match the name in Package.swift - that's another extra point to watch out for.
  6. You mentioned no folder structure is needed, but it's not known for me did you imply that literally or not, as a certain (yet minimal) folder structure is present in your example project. Especially given the contradictory information here which discusses a certain more rigid and deep folder structure. To figure out what structure to follow would take me quite a time.
  7. Then there are potential unknowns to me. Like "source only" modules vs "binary" modules. Or "swift only" modules vs "mixed language" ones. Or local modules vs remote modules. Or versioning. Or when to clean package cache and use other commands. Or even is module and package the same thing if so why use a different name :slight_smile:. I don't know these unknowns yet to know if I need to be concerned or not.
  8. There are quite a few variables here (names, structure, "If you are using a different build system" (am I ?! should I ?!) so if something goes wrong (e.g. I renamed a target, or clicked a wrong button somewhere in Xcode UI, etc) it would take quite a while for me to figure out what's going on and how to fix it.
  9. To be on the safe side I'd need to explore and learn this topic first, and it looks like it could take a few days to know it good, compared to Bridging header option learning which only takes one page to read and half an hour to play with.

All in all, I can't possibly see at this point how bridging header is harder. I'm sure if I used modules on a daily basis for a few days or weeks the two approaches would be equally easy for me, but even once I am fluent with it objectively module approach would still require more steps to follow and more variables to watch out for.

Glad to be of help.

Unfortunately, my experience with VSCode is only as deep as I've followed a guide to setup, test, modify the Swift plugin. My Xcode knowledge is that is the IDE that people on the Apple platforms use. Android Studio is some large Java application, and that Visual Studio was the best IDE when I last tried it in the 90s. My normal development environment is a terminal and vim :slight_smile: No LSP, no indexing, no fancy tools. Just an editor and a console.

From an inexperienced user point of view:

  • designed to bridge Objective-C frameworks into Swift
  • changes the language processing rules
  • potentially adds additional declarations that confuse things
  • does not allow you to segment the imports
  • this has a learning curve to understand how it interacts with the rest of the system
  • may result in overlinking
  • can adversely impact load times
  • Package.swift has nothing to do with the modules
  • The only flags you need to know about are -I to add the include path (depends in Package.swift, target_link_libraries in CMake, or deps in Starlark)
  • The only targets to figure out are the ones that you need to build and is part of why modules are better - they are going to prevent cross-project pollution and will help your project build faster
  • The structure in the project is pretty much what nearly every single non-university homework project does: has an include directory and the sources. There is no real structure that is imposed.
  • I think all the other concerns that you raise about how apply just as much to bridging header

It seems to me that your argument is that you are familiar with how to use a bridging header and therefore you are recommending it rather than modules that you haven't really bothered to explore. That is how knowledge is shared but doesn't necessarily equate to a good solution. Exploring alternatives and understanding the tradeoffs are part of software engineering.

Using modules really requires nothing more than adding the module definition (module.modulemap) and adding it to your search path (something which is required pretty much in both solutions). With implicit modules as part of SPM, you do not need to write the module.modulemap, only add the target to your build definition. Given that it is the Swift Forums, I did presume that they are working with Swift code, and the general way to build Swift code is with SPM, and at that point, the only reference that is needed is that you define a target in your build manifest.

I don't quite understand the discussion about -import-objc-header. My understanding was, that ObjC interop is only available in the proprietary version of the Swift Runtime and therefore unavailable on non-Darwin platforms. Years ago I did even try to install GNUStep in a faint hope that it will magically enable ObjC interop. :sweat_smile:

Am I correct to assume, that -import-objc-header is used on non-Darwin platforms to import a C headers and the "objc" is just a poor naming?

That's my understanding. On Apple platforms you can put objc stuff in there so the name kind of makes sense, I also had no problems including normal C header from a bridging header (or just using C header as is as the bridging header) – on Apple platforms, I do not have other platforms to test, perhaps OP (or anyone sufficiently motivated with the access to other platforms) could confirm if this works and especially if it doesn't.

The headline post asked among other things if bridging header could be used outside Xcode, and I did my best to help. Looks like OP is happy with the answer, so far.

1 Like

This might be a good time for an update from my side.

First of all, I would also assume that "objc" is just some kind of poor naming. Nevertheless, i was only talking abot "bare" C code so i did not check if Objective-C would work also. However, regarding the usability on other platforms than Apples Darwin we had some drawbacks yesterday by adding more of our C stuff to the bridging header.

The example above works flawlessly on all patforms independend from the enrionment which had been used. The C-struct can be imported and used how its intended.
One note may be, that on Windows, we had to change the #import statement in the bridging header to #include as the compiler told us that "#import of type library is an unsupported Microsoft feature".

However, as soon as we add c functions it does work only using Xcode. Every attempt to use CMake fails, even on Darwin as linking fails due to an unresolved external symbol referring to the functions name. Like said before, doing this inside Xcode is fine.

Somehow we don't feel like giving up yet, but then again maybe this is proof that this is inteded to be used just on Darwin. :man_shrugging:

Just for completion, if someone wants to give it a try:
cct.c

//  cct.c
#include "cct.h"

int doublemyint(int a) {
    return a*2;
};

cct.h

//  cct.h
#ifndef cct_h
#define cct_h

struct astruct {
    int a;
};
int doublemyint(int a);

#endif /* cct_h */

sctest.swift

//  sctest.swift

import Foundation

var astr=astruct()
astr.a = 1
print(astr)

var a:CInt
var b:CInt
a=2
b=doublemyint(a)

sctest-Bridging-Header.h

#include "cct.h"

CMakeLists.txt

CMAKE_minimum_required( VERSION 3.20 )

enable_language(Swift)

project( SwiftTest VERSION 0.1.0 )

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/lib")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")

file( GLOB_RECURSE main_src ${CMAKE_CURRENT_SOURCE_DIR}/sctest.swift)

add_executable(sctest ${main_src})

target_compile_options(sctest PRIVATE -import-objc-header ${CMAKE_CURRENT_SOURCE_DIR}/sctest-Bridging-Header.h)

I found this image on some website in Japanese.

If this diagram is correct, any .h file will "go" into ClangImporter which I assume only produces additional source of truth of declarations available during Semantic Analysis.

In order to build your function, the ClangImporter would need pass down your C code and the swiftc would need to be a full-featured C compiler, which is afaik not the case. In your CMakeLists.txt, I don't see any linking (or build step, although I'm not that familiar with cmake) of your C code. Therefore I would expect this to fail exactly in this manner.

As what happens in Xcode, I have no idea.

Bridging header itself works - you are past the compilation phase so. As for the linking error perhaps it's just a matter of adding the corresponding ".c" or ".o" file to cmake flags.