Skip to main content
supersonicbyte

Swift interoperability with C (Part 1)

Intro

The C programming language is the backbone of modern computing. With its assembly-close nature offering speed and effectiveness it's still the main choice when writing system software since it's beginning is the '70s. Most operating systems are written in C. For any new programming language, it's crucial to have a way to communicate and integrate with existing C code.

Swift comes with exceptional interoperability with Objective-C/C and most recently with C++.

This is the first part of a series of blog posts dedicated to Swift's interoperability with C. In this post we're going to explore how does actually Swift call into C code.

Creating a simple C library

Let's create a simple C library. Consider the following C code:

#include <stdio.h>

void foo() {
  printf("foo!\n");
}

And the header:

#ifndef FOO_H
#define FOO_H

void foo();

#endif

Note: The #ifndef and #define and the beginning are called include guards.

Pretty straightforward. We define a simple function foo which prints to the standard output.

Let's compile it:

clang -c foo.c

This will compile our code and produce a foo.o object file, which contains the actual machine code. If we run:

Note: I'm on a macbook and I'm using the Clang compiler.

file foo.o

We get:

Mach-O 64-bit object arm64

Now, we just need to archive the file to make it into a static library. We can do so by running:

ar -rv libfoo.a foo.o

A static library is an archive of .o files with a .a extension

Great! Now let's try to use our newly created library in a test program:

#include <stdio.h>
#include "foo.h"

int main(int argc, char *argv[])
{
  foo();
  return 0;
}

We can now compile and run:

clang test.c libfoo.a -o Test
./Test
foo!

Alright, our C library is working well. Now we can try to call it from Swift!

Importing C in Swift

Create a new swift file, name it main.swift.

print("Hello from Swift!")

foo()

print("Goodbye!")

We just referenced the foo function as if it was imported, but naively trying to compile it with swiftc will fail. We are referencing foo which hasn't been declared anywhere. We have to express that foo should be imported from our libfoo C library. In our C program we did so by including the foo.h header which declares our foo function.

One way of doing it is to pass the header file to the compiler with the -import-objc-header flag. The other, preferred way, is with Clang modules. Clang modules are a concept from the Clang compiler. They bring a lot of improvements and solve pain points of plain header files. We won't go in detail right here, but if you want to know more about them you can find it in the documentation.

Let's create a clang module for our foo.h by making a file named module.modulemap and setting it's contents to:

module CFoo {
  header "foo.h"
  export *
}

The modulemap is pretty simple, it declares a module named CFoo (it's a convention that we add a prefix C for C modules that we import in Swift) references our foo.h header and exports everything from it.

Now, let's import this module we created by in our main.swift by adding this on the top:

import CFoo

Great! We are all set. The last thing to do is to invoke the swift compiler and pass all the required information for it to compile and link our main.swift:

swiftc -I ./ -L ./ -lfoo  main.swift -o main 

Note: If we wanted to use plain header we would invoke the compiler like this: swiftc -import-objc-header foo.h -I ./ -L ./ -lfoo main.swift -o main

Voila! And if we run our executable we get:

./main
Hello form swift!
foo!
Goodbye!

Everything works!

Let's break down the flags we are sending to the compiler:

Let's do some unwinding. On a high level, the swift compiler parses the passed in clang modules and transforms the declarations in the C headers to Swift declarations. The result of this is that our foo from C will end up as a swift declaration:

func foo()

But these imported C functions get very different treatment during the compilation from Swift functions. Their names don't get mangled so they preserve C calling convention (more on that later). Mangling is the process of encoding functions and variable names into unique names that are later on useful in the linking process. One thing mangling enables for us is function overloading, since every argument will be encoded into the mangled name thus allowing us to overload functions.

For example, consider the following function:

func bar(arg: Int)

After mangling the function name will be:

$s3barAA3argySi_tF

To demangle a name we can use:

swift demangle s3barAA3argySi_tF
// it prints out the following
$s3barAA3argySi_tF ---> bar.bar(arg: Swift.Int) -> ()

(Note that we dropped the $ prefix since that's a special character in most shells)

We mentioned that we want to perserve C calling convention. The calling convention defines the following:

Basically, it defines the exact way how we should setup our machine code when calling machine code that was outputed from compilaton our C code. For any two pieces of machine code that interact, it's important that the convention is aligned in other for the caller and callee to work as expected. That being said, in order for machine code generated code from Swift to correctly call a machine code generated from a C source it's important to follow the C calling convention. The convention could possibly differ regarding which underlying platform is targeted but it's important that the conventions are aligned. C doesn't support function overloading and mangling, and that's way it's important that this function name doesn't get mangled. Along the way the imported C functions also get annotated with @convention(c) (this can be seen if we print out the SIL Swift Intermidiate Language). This is also plays into the compiler treating these functions specially so it can generate adequate machine code which follows the calling convention.

Since we are linking statically against our libfoo the linker will actually embed the code that we are referencing from library into the executable and then the resolve the foo call with an actual memory address of the function.

Let's confirm this by examining the executable:

objdump -d main | grep 'foo'

This will disassemble the machine code into assembly and then we search for the foo symbol. And we can see:

...
100003d94: 94000056    	bl	0x100003eec <_foo>
...

This is the place in our main.swift where we are calling foo. In the assembly bl denotes an arm64 (this could be different depending on what platform are you executing e.g. Intel Mac) instruction called branch and link. Before branching the instruction stores the current address into the link register which is used to keep track to which address to return when the ret instruction is being executed. The instruction has one argument which determines the address to which the execution should be branched.

If we search again we can find the definition of foo in the assembly.

0000000100003eec <_foo>:
100003eec: a9bf7bfd     stp     x29, x30, [sp, #-0x10]!
100003ef0: 910003fd     mov     x29, sp
100003ef4: 90000000     adrp    x0, 0x100003000 <_swift_bridgeObjectRelease+0x100003000>
100003ef8: 913dbc00     add     x0, x0, #0xf6f
100003efc: 9400000f     bl      0x100003f38 <_swift_bridgeObjectRelease+0x100003f38>
100003f00: a8c17bfd     ldp     x29, x30, [sp], #0x10
100003f04: d65f03c0     ret

After all the instructions are executed we hit the ret instructions which takes us back to the address stored in the link register, meaning we finished the with the call of foo and we are returning to execution of our main.swift.

And at this point exactly we understand how the Swift code called our C function.

Final notes

Interoperability boils down to making the machine code generated from different languages play nicely with each other. A lot of heavy lifting is done inside the Swift compiler, which seamlessly translates the C declarations and exposes them as Swift declarations. This enables an incredible developer experience with automatic translations of C declarations to Swift declarations and autocompletion of imported C code in Xcode.

In this post we explored the bare bones way of interoperability of Swift and C without a sophisticated build system, which can be quite tedious. We did so for better understanding of what's going on on the lower level. Usually, we don't have to deal with the compiler directly and we use a more sophisticated build system, most commonly the ones from Xcode and SPM. We also explored the most basic C declaration and haven't covered more advanced types and how they map into Swift.

Part 2 of this blog series will try to explain:

Until next time, M.