Swift & C Interop

@attheodo

The marriage of two beasts

Types

Arrays & Structs

//header.h
char name[] = "IAmAString"; 👌

// file.swift
print(name) // (97, 115, 100, 100, 97, 115, 100, 0) 🤪

Arrays

Structs

//header.h
typedef struct {
    char name[5];
    int value;
    int anotherValue;
} MyStruct;

// file.swift
public struct MyStruct {
    var name: [UInt8]
    var value: Int32
    var anotherValue: Int32
    
    init()
    init(name: [UInt8], value: Int32, anotherValue: Int32)

}

Enums & Constants

//header.h
typedef enum ConnectionError{
    ConnectionErrorCouldNotConnect = 0,
    ConnectionErrorDisconnected = 1,
    ConnectionErrorResetByPeer = 2
}

// file.swift
struct ConnectionError : RawRapresentable, Equatable{ }

var ConnectionErrorCouldNotConnect: ConnectionError {get}
var ConnectionErrorDisconnected: ConnectionError {get}
var ConnectionErrorResetByPeer: ConnectionError {get}

Enums

Contants

// header.h
#define MY_CONSTANT 42

// file.swift
let MY_CONSTANT 42

More complex macros will get ingored by Swift

Calculating sizes

In Swift you can obtain the data-only or memory size of a specific type (primitive or compound) using the MemoryLayout<T> generic struct and the properties and functions it provides.

print(MemoryLayout<CChar>.stride)  // 1 byte

struct AStruct {
    let anInt8: Int64
    let anInt: Int16
    let b: Bool
}

print(MemoryLayout<AStruct>.size))    // 11 (8+2+1) byte
print(MemoryLayout<AStruct>.stride)   // 16 (8+4+4) byte

Most of the times you should be working with stride

Null-ability

Thankfully, null gets translated to nil

For methods that expect pointers, you can pass nil directly... it will automatically translate to UnsafeRawPointer?

If you need a typed null pointer:

let p: UnsafeMutablePointer<UInt8>? = nil

Brace yourselves... more pointers coming...

Pointers

Pointers get automatically translated to different kinds of Unsafe*

Pointers

Pointers

  • General rule: mutable pointer instances point to mutable vars.

  • For class objects (i.e NSDate**), pointers to object passed as pointers translate to AutoreleasingUnsafeMutablePointer

  • If the type we are pointing to is not completely defined or cannot be represented in Swift, the pointer will be translated to OpaquePointer.

    • Values pointed to by OpaquePinter cannot be access directly! The pointer variable will have to be converted first.

  • Untyped pointers get translated to UnsafeRawPointer

  • All pointer types, except of raw and opaque pointers, are type safe.

    • The compiler will perform type checks on the pointer and its contents

    • The compile will also guarantee address alignment with the Pointee type they point to.

  • The Unsafe prefix has to do with how we access the content.

    • Interacting with pointers bypasses the general Swift's safety methods so, yes, you can crash something even if it passes compilation.

Pointers

Once you have a non-void Unsafe*

var anInt:Int = myIntPointer.pointee
myIntPointer.pointee = 42
myIntPointer[0] = 42
  • pointee is used to access the instance references by the pointers
  • You can also access any element in a sequence of pointers using the "subscript" notation

Pointer states

  • Unallocated. No pointer reserved for the pointer.
  • Allocated. The pointer points to a valid memory location but its value is not initialized yet.
  • Initialized. The pointer points to an allocated and initialized location.

Pointer states

var ptr = UnsafeMutablePointer<CChar>.allocate(capacity: 10)
ptr.initialize(from: [CChar](repeating: 0, count: 10))
ptr[3] = 42
ptr.deinitialize()
ptr.deallocate(capacity: 10)
  • Swift won't take the last two steps for you...
  • You allocated and initialized it...
  • ... it's your responsibility to clean up

Pointer Conversion

  • Temporarily bind the content pointed by a pointer to a different "view" of the data
let charPtr = ptr.withMemoryRebound(to: CCChar.self, capacity: 11) {
    (cptr) -> String in
    return String(validatingUTF8: cptr)!
}
  • For permanent pointer conversion you need raw types
UnsafeRawPointer(typedPointer).bindMemory(to: UIInt8.self, 
                                          capacity: 1024)

Pointer Arithmetic

  • Classic C way of moving through sequences
  • You can do it in Swift too!
var ptr = UnsafeMutablePointer<CChar>.allocate(capacity: 5)
ptr.initialize(from: [1,2,3,4,5])

print(ptr.successor().pointee) // 2
print(ptr.advanced(by: 3).pointee) // 4
print(ptr.advanced(by: 3).predecessor().pointee) // 3

aptr.deinitialize()
aptr.deallocate(capacity: 5)

Pointer Arithmetic

  • You can still use integers for inc/dec pointers
var ptr = UnsafeMutablePointer<CChar>.allocate(capacity: 5)
ptr.initialize(from: [1,2,3,4,5])

print((ptr + 1).pointee) // 2
print((ptr + 3).pointee) // 4
  • Note: When you inc/dec an Unsafe(Mutable)Pointer, the actual pointer is moved by multiples of MemoryLayout<Pointee>.alignment
  • Raw or opague pointers are just inc/dec by the give number of positions

Working with strings

  • When a C function has a string parameter (char pointer), this parameter gets imported to Swift as Unsafe(Mutable)Pointer<Int8>
  • Swift automagically converts strings into pointers pointing to UTF8 buffers, so you can actually pass a Swift string directly to function expecting char pointer parameters.
  • But you can also turn Swift strings into the C representation for i.e doing any pointer magic

Working with strings

// Passing a Swift string to a libc method
puts("foobar")

var str = "Hello CocoaHeads"
str.withCString { (ptr: UnsafePointer<Int8>) -> Void in
    // do something with the pointer
}

// Convert C strings to Swift strings
let swiftString = String(cString: aCString)!

C memory and ARC

  • Imagine we pass a Swift reference object to a C function that returns its result in a callback
  • Are we sure that the Swift object will be still allocated when the callback is returned?​
    • ​Nope.
  • Say hello to Unmanaged
    • ​With Unmanaged you are able to directly alter the retain count of an object and convert it to an OpaquePointer if you need to pass it around

C memory and ARC

// foobar.c
void aCFunctionWithContext(void *ctx, void (*function)(void *ctx)) {
    sleep(10);
    function(ctx);
}
  • Here's a C method that returns it's result in a callback

C memory and ARC

class MyClass: CustomStringConvertible {
    var property: Int = 0
    var description: String { return "MyClass with property \(property)"
}

var myClass = MyClass()

let unmanaged = Unmanaged.passRetained(myClass)
let opaquePtr = unmanaged.toOpaque()
let ptr = UnsafeMutableRawPointer(opaquePtr)

aCFunctionwithContext(ptr) { (p: UnsafeMutableRawPointer?) -> Void in
    var c = Unmanaged<MyClass>.fromOpaque(p!).takeUnretainedValue()
    c.property = 2
    print(c) // "MyClass with property 2"
}

C memory and ARC

  • passRetained allows us to retain a given object by incrementing its reference count
  • Since the C callback needs a void pointer, we first obtain an OpaquePointer with the toOpaque() method and then we convert it to UnsafeMutableRawPointer
  • Inside the callback, we perform the reverse operation to get back our MyClass instance from the raw pointer

In practice

public enum KafkaClientType {   
    case consumer
    case producer
    
    func toRawType() -> rd_kafka_type_t {       
        switch self {       
        case .consumer:
            return RD_KAFKA_CONSUMER
        case .producer:
            return RD_KAFKA_PRODUCER
        
        }       
    }   
}

let kSwiftKafkaCStringSize = 1024
let errString = UnsafeMutablePointer<CChar>.allocate(capacity: kSwiftKafkaCStringSize)
defer {
   errString.deallocate(capacity: kSwiftKafkaCStringSize)
}
let handle: OpaquePointer = rd_kafka_new(clientType.toRawType(),
                                         globalConfiguration!.handle,
                                         errString,
                                         kSwiftKafkaCStringSize) 

In practice

public func getMetadata(forTopicHandle topicHandle: OpaquePointer? = nil, 
                        timeout: Int32 = 1000) throws -> Metadata 
{
        
        guard let h = handle else {
            throw KafkaError.unknownError
        }
        
        let ppMetadata = UnsafeMutablePointer<UnsafePointer<rd_kafka_metadata>?>.allocate(capacity: 1)
        var error: rd_kafka_resp_err_t
        
        if let topicHandle = topicHandle {
            error = rd_kafka_metadata(h, 0, topicHandle, ppMetadata, timeout)
        } else {
            error = rd_kafka_metadata(h, 1, nil, ppMetadata, timeout)
        }
        
        guard error == RD_KAFKA_RESP_ERR_NO_ERROR else {
            throw KafkaError.coreError(KafkaCoreError(error))
        }
        
        guard let rawMetadata = (ppMetadata.pointee)?.pointee else {
            throw KafkaError.unknownError
        }
        
        rd_kafka_metadata_destroy(ppMetadata.pointee)
        ppMetadata.deallocate(capacity: 1)
        
        return Metadata.metadata(fromRawMetadata: rawMetadata)
        
}

In practice

extension Metadata {
   
    static func metadata(fromRawMetadata data: rd_kafka_metadata) -> Metadata {
        
        let brokers = BrokerMetadata.brokers(fromRawBrokersData: data.brokers, 
                                             numOfBrokers: data.broker_cnt)
        let topics = TopicMetadata.topics(fromRawTopicsData: data.topics, 
                                          numOfTopics: data.topic_cnt)
        
        return Metadata(brokers: brokers,
                        topics: topics,
                        originatingBrokerId: Int(data.orig_broker_id),
                        originatingBrokerName: String(cString: data.orig_broker_name))
        
    }

}

In practice

public struct BrokerMetadata {
    public let id: Int
    public let host: String
    public let port: Int
}

extension BrokerMetadata {
    
    static func brokers(fromRawBrokersData data: UnsafeMutablePointer<rd_kafka_metadata_broker>,
                        numOfBrokers: Int32) -> [BrokerMetadata]
    {
        
        var brokers: [BrokerMetadata] = []
        
        guard numOfBrokers > 0 else {
            return brokers
        }
        
        for i in 0...numOfBrokers - 1 {
            
            let b = data.advanced(by: Int(i)).pointee
            let broker = BrokerMetadata(id: Int(b.id), 
                                        host: String(cString: b.host), 
                                        port: Int(b.port))
            
            brokers.append(broker)
            
        }
        
        return brokers
        
    }

}

Bonus

  • Exposing your C code as a Swift module
// Package.swift
let package = Package(
    name: "ckafka",
    // Name of the pkg-config (.pc) file to get the 
    // additional flags for system modules.
    pkgConfig: "rdkafka",
    providers: [
      .Brew("librdkafka"),
      .Apt("librdkafka-dev")
    ]
  )

// Module.modulemap
module ckafka [system] {
  header "ckafka.h"
  link "rdkafka"
  export *
}

Questions?

@attheodo

http://attheo.do

Made with Slides.com