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:
-I
flag enables us to pass a directory which will be used as theimport search path
, basically a place where it tries to resolve our imports, we pass the current directory (because we created ourmodule.modulemap
in the same directory as ourmain.swift
)-L
flag enable us to pass a directory which will be used as thelibrary link search path
, a place where to look for the libraries we are linking against, again we pass the current directory for the same reasons-lfoo
flag is passed down to the linker and it tells the linker to search for a library namedlibfoo
in the library search path. More about this flag can be found in the man pages of ld.main.swift
the source file we are compiling-o
specifies the name of our output
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:
- how the arguments are passed onto the stack
- who will clear the stack, caller or callee?
- what registers will be used and how
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:
- how we can leverage SPM to build mixed language targets
- more advanced C integrations
- how to go the other way around - calling Swift code from C.
Until next time, M.
- ← Previous
SKTestSession testing failures