Skip to content

Instantly share code, notes, and snippets.

@HiImJulien
Last active March 30, 2024 19:00
Show Gist options
  • Star 59 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save HiImJulien/c79f07a8a619431b88ea33cca51de787 to your computer and use it in GitHub Desktop.
Save HiImJulien/c79f07a8a619431b88ea33cca51de787 to your computer and use it in GitHub Desktop.
This gist is a simple example on how to call a function written in swift from C/C++, without taking the detour via Objective-C/C++.

Swift Meets C/C++

This gist is a simple example on how to call a function written in swift from C/C++, without taking the detour via Objective-C/C++.


Analytics

In this example we're going to invoke a function called say_hello, which, as the name already suggests, prints "Hello, World!" to the terminal.

say_hello is defined as following:

// func.swift

@_cdecl("say_hello")
public func say_hello(){
    print("Hello, World!")
}

This is just the canonical "Hello, World!"-example wrapped in a function called say\_hello. The only thing, that stands out, is the attribute @_cdecl("say\_hello").

@_cdecl is an undocumented attribute, which according to vague informations found on the web, enforces C-name mangling. Note: @_cdecl solely enforces the name mangling; not the calling convention!

In order to link our code with a C/C++ application, will need a library. In order to compile our code as a linked library, specify -emit-library when compiling the .swift-file.

We want to link our swift code with our C/C++ applications, which means, that we'll need this code to be compiled as a library. In order to compiler our code as a linked library, just specify '-emit-library' when compiling the .swift-file.

swiftc func.swift -emit-library

This will result in a linked library called "libfunc.dylib" (or "libfunc.so" on Linux, I suppose), which exports our function say_hello as _say_hello.


Now to our C++:

// main.cpp

// only "extern" when targeting C.
extern "C" void say_hello();

int main(){
    say_hello();    // Prints "Hello, World!"
    return 0;
}

Here we declare say_hello as an external reference, which will be provided by the library, we compiled before.

What's left at this point is just to compile and link our code.

gcc -std=c++11 -c main.cpp -o main.o 
gcc libfunc.dylib main.o -o main

When we run the application, we're getting greeted with following:

./main
> Hello, World!

This was my first "technical article". Feedback is welcome!

None the less, I hope I could help you with this "article".

J. Kirsch

Resources

  • SO Answer by Max Desiatov
  • Trial'n'Error with the command line tool nm
  • Other links, that I cannot remember... :-(
@HiImJulien
Copy link
Author

I tinkered around with it recently and found, that the @_cdecl directive indeed also forces the cdecl-calling convention, which trivially enables us to pass arguments. See following example:

main.cpp:

#include <cstdint>

extern "C" void print_num(std::int32_t);

int main() {
    print_num(2);
    return 0;
}

lib.swift:

@_cdecl("print_num")
public func print_num(num: Int32) {
    print("The number is \(num)")
}

build.ninja:

builddir = bin
cc = gcc
cflags = -std=c++17
ldflags = -lstdc++

sc = swiftc
sflags = -emit-library

rule compile_cpp
    command = $cc $cflags -c $in -o $out

rule compile_swift
    command = $sc $sflags $in -o $out

rule link
    command = $cc $ldflags $in -o $out

build $builddir/main.o: compile_cpp main.cpp
build $builddir/lib:    compile_swift lib.swift
build $builddir/link:   link $builddir/main.o $
                             $builddir/lib $

default $builddir/link

For those who do not use the ninja build system (and have no idea what is going on above), you can compile this example using following terminal commands

gcc -std=c++17 -c main.cpp -o bin/main.o
swiftc -emit-library lib.swift -o bin/lib
gcc -lstdc++ -c bin/main.o bin/lib -o link

P.S.: I hacked this example out of my memory; there could be some trivial mistakes.

@marc-medley
Copy link

marc-medley commented Jul 29, 2019

In the second example, i found the last command to fail with the following warning:

gcc -lstdc++ -c bin/main.o bin/libframework.dylib -o link

# clang: warning: -Z-reserved-lib-stdc++: 
#    'linker' input unused [-Wunused-command-line-argument]
# clang: warning: bin/main.o: 
#    'linker' input unused [-Wunused-command-line-argument]
# clang: warning: bin/lib: 
#    'linker' input unused [-Wunused-command-line-argument]

However, the following did work:

swiftc -emit-library lib.swift -o bin/lib
gcc -std=c++17 -c main.cpp -o bin/main.o
gcc bin/main.o bin/lib -lstdc++ -o bin/linked

./bin/linked
# The number is 2

Alternately, a g++ approach also works.

swiftc -emit-library lib.swift -o bin/lib
g++ -std=c++17 -c main.cpp -o bin/main.o
g++ bin/main.o bin/lib -o bin/linked

./bin/linked
# The number is 2

As FYI, StackOverflow has some discussion of "What is the difference between g++ and gcc?", and "Which one should be used for general c++ development?".

@Raj123456788
Copy link

Can you please tell me where to add this in swift project: "swiftc func.swift -emit-library" I am trying to call a swift from C++ but it keeps throwing me error: Unable to find print_num

@narektutikian
Copy link

Can you please tell me where to add this in swift project: "swiftc func.swift -emit-library" I am trying to call a swift from C++ but it keeps throwing me error: Unable to find print_num

@Raj123456788 If I get the question correctly ,swiftc func.swift -emit-library is the command for building the swift code into binaries. It is not the part of the swift code but command for running in the terminal.

@narektutikian
Copy link

Thank you for posting this. I wish I found this a weak ago.

@neopolitans
Copy link

neopolitans commented Oct 15, 2022

Context: I came to here looking for help days ago with no knowledge of Swift, limited knowledge of Xcode13 and a book apple gives out for free. After a few days of just non-stop working with Swift to understand it's similarities to Luau, C# and C++ (heresy, I know), I came back to start learning and working with @_cdecl Stuff here may be wrong. I'm not 100% sure on everything. This is what worked for my project, I want it to be publicly available especially if I forget how to do it again.

This is to be used in conjunction with OP's post.

For those who want to do this but are using a fresh copy of Xcode 13, no external compiler tools outside of Terminal and no understanding of Terminal itself, here's two notes for you that I had to figure out myself.

  • To compile the swift file into a library, you have to navigate to the project folder using the cd ~/ command to go recursively through the directory folders to it. e.g. cd ~/Documents/ExampleProject/ExampleProject/SwiftFiles. When you're at the directory for the target file to be compiled, you run the command that the author posted for compiling swiftc <swiftFileNameHere>.swift -emit-library. It will open up Xcode and compile one of the two library formats from that. Note: cd ~/ is akin to going directly to your Users\YourLocalUsernameHere so if you've copied the full file path directly from Xcode 13's file properties tab, you can omit that.

  • To import it in Xcode 13, you need to have a C/C++ project already. When you have a C/C++ project, you need to click on the .xcproj file in the hierarchy on the left. In the library, you want to go to either General > Frameworks and Libraries or Build Phases > Link Binary with Libraries

    • For those going via General > Frameworks and Libraries, you want to click on the + icon in the bottom left corner of the section and then Add Other.... A file browser will come up and you need to navigate to and select the dylib/so file you wish to link. When it's appeared in the list, set it's Embed property to Embed without Signing or Embed & Sign. You may need to go into Build Phases > Link Binary with Library to check if the file has also been added as a Required file and not an Optional one.
    • For those going via Build Phases > Link Binary with Library you repeat the same Add Other... step as with above. When the file appears within the list here, you want to check if the file has been added as a Required file. Once done, go to the General > Frameworks and Libraries tab and set the dylib/so file's Embed property to Embed without Signing or Embed & Sign.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment