Use C-function in swift

I want to use a C-function in my Swift project, this function should populate a Swift String array, see please the following simple example with a Swift function:

import Foundation

func fillArray(_ cmd: inout [String]) -> Int {
    cmd.append("Zero")
    cmd.append("One")
    cmd.append("Two")
    cmd.append("Three")
    cmd.append("Four")
    return 5
}

func manageCommand(cmdLine: String) -> Void {
    var cmd = [String]()
    print(cmdLine)
    var number: Int = fillArray(&cmd)
    print("\(number) elements")
    print(cmd)
}

…which gives the following result:

5 elements

["Zero", "One", "Two", "Three", "Four"]

I would like to get the same result by calling a C-function shown below:

#include <stdio.h>
#include <string.h>

// Not objectivec but C
int c_fillArray(char arr[][10]) {
    strcpy(arr[0], "Zero");
    strcpy(arr[1], "One");
    strcpy(arr[2], "Two");
    strcpy(arr[3], "Three");
    strcpy(arr[4], "Four");
    return 5;
}

and the Swift code calling this function:

import Foundation

func manageCommand(cmdLine: String) -> Void {
    var cmd = [String]()
    print(cmdLine)
    var number: CInt = c_fillArray(cmd) // This line creates an error
    print("\(number) elements")
    print(cmd)
}

Xcode gives me the following error:

Cannot convert value of type '[String]' to expected argument type 'UnsafeMutablePointer<(CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)>' (aka 'UnsafeMutablePointer<(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)>')

I already search the internet to find how to let it work but without success, thanks for your help

1 Like

This will work:

typealias TenChars = (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)
let zeroTenChars: TenChars = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

func manageCommand(cmdLine: String) -> Void {
    print(cmdLine)
    let cmd = UnsafeMutablePointer<TenChars>.allocate(capacity: 100)
    cmd.initialize(repeating: zeroTenChars, count: 100)
    let number: CInt = c_fillArray(cmd)
    let items = (0 ..< Int(number)).map { i in
        withUnsafeBytes(of: &cmd[i]) { p in
            String(cString: p.baseAddress!.assumingMemoryBound(to: CChar.self))
        }
    }
    cmd.deinitialize(count: 100)
    cmd.deallocate()
    print("\(number) elements")
    print(items)
}

There's possibly a cleaner way to do this without declaring a tuple and/or using a recently introduced InlineArray instead of a tuple.

Thx, I just replaced my function with what you submitted but I get another error now:

Cannot find 'c_fillArray' in scope

Do you have a prototype exposed in the birding header directly or indirectly?
Typically that would be living in (following your file names) "functions.h" that both "functions.c" and bridging header include.

Taking some steps back….

Is it you who developed the C code? Or is it someone else’s C code?

if it is you, do you use C for the purpose of performant shared code between Swift consumer and possibly some other consumer (eg Android app (Kotlin))?

depending on your answers I might be able to recommend another solution for you :)

1 Like

May be, it’s the first time I try to use c-functions with Swift. I just added a dummy function into functions.c:

int c_add(int a, int b){
    return a + b;
}

I call it from swift using var res: CInt = c_add(3, 4)

functions.h contains:

int c_add(int a, int b);
int c_fillArray(char arr[][10]);

…I’ve #import "functions.h" in TinyForth-Bridging-Header.h

…and I get following errors:

Undefined symbol: _c_add

Linker command failed with exit code 1 (use -v to see invocation)

As this is a linker error I'd recommend you to check what's in "Build phases" / "Compile Sources", do you have functions.c file there or not.

Also is "functions.c" file treated as "C Source" (this is default) or "C++ Source" which could also explain the linker error.

1 Like

Yes the C-code are just simple functions to test how I can use self made c-function in swift, C-code and swift-code are in the same Xcode project.

Thx you’re right, function.c was not in compiled sources but now it is and the code you sent works well but I honestly don’t understand how it works and a cleaner way would be welcomed

The

    let cmd = UnsafeMutablePointer<TenChars>.allocate(capacity: 100)
    cmd.initialize(repeating: zeroTenChars, count: 100)
    ....
    cmd.deinitialize(count: 100)
    cmd.deallocate()

part is used to allocate C-compatible layer out array of chars.

With the new InlineArray (needs Swift 6.2, which requires Xcode 26, which in turn requires macOS 15.6 or higher) you could make the whole thing slightly simpler:

typealias TenChars = (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)

func manageCommand(cmdLine: String) -> Void {
    print(cmdLine)
    var cmd: [5 of [10 of CChar]] = .init(repeating: .init(repeating: 0))
    let number = withUnsafeMutableBytes(of: &cmd) { p in
        let param = p.baseAddress!.assumingMemoryBound(to: TenChars.self)
        return c_fillArray(param)
    }
    let strings = (0 ..< Int(number)).map { i in
        makeString(cmd[i])
    }
    print(strings)
}

func makeString<let N: Int>(_ items: InlineArray<N, CChar>) -> String {
    var items = items
    return withUnsafeBytes(of: &items) { p in
        String(cString: p.baseAddress!.assumingMemoryBound(to: CChar.self))
    }
}

although it's not any shorter, still needs a tuple, and is not quite as simple as it could be in theory, pseudocode:

    var cmd = [5 of [10 of CChar(0)]]
    let number = c_fillArray(&cmd)
    let strings = (0 ..< Int(number)).map { i in
        String(cString: cmd[i])
    }

typealias TenChars = (CChar, CChar, … (10 times)

If I understand well,

<10> is the maximum length of string but I would like to have maximum length of 32!

and <100> is the maximum array capacity?

Right. Good you don't want 1024 as it would be quite painful to write as a tuple...
Also beware about strings of the maximum size (e.g. 32 chars as you indicated) - the current code assumes terminating 0 being present in the string (both strcpy and String(cString)) and if the string itself is 32 chars there's no space for the trailing 0.

As a side note: now that we have InlineArray we should probably have some way of opting-in to use it instead of tuple as a currency type for C arrays.

and <100> is the maximum array capacity?

Yes. Obviously bad things will happen if you pass a long string:

    strcpy(arr[4], "Long String Here");

or a big index:

    strcpy(arr[123], "Zero");
1 Like

I replaced <100> with <MAX_ITEMS> which works well but trying to change the string size from 10 to 32 causes an error!

let MAX_ITEMS = 64
typealias nChars = (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar) // 32 <CChar>
let zeroNChars: nChars = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // 32 <0>

func manageCommand(cmdLine: String) -> Void {
    print(cmdLine)
    let cmd = UnsafeMutablePointer<nChars>.allocate(capacity: MAX_ITEMS)
    cmd.initialize(repeating: zeroNChars, count: MAX_ITEMS)
    
    let number: CInt = c_fillArray(cmd)
    let items = (0 ..< Int(number)).map { i in
        withUnsafeBytes(of: &cmd[i]) { p in
            String(cString: p.baseAddress!.assumingMemoryBound(to: CChar.self))
        }
    }

    cmd.deinitialize(count: MAX_ITEMS)
    cmd.deallocate()
    print("\(number) elements")
    print(items)
}

You probably still have 10 in C api.

Right! Thx a lot for your help

No worries!

The question to those in the know. This:

int c_fillArray(char arr[][10]);

is now imported to Swift as this:

func c_fillArray(_ arr: UnsafeMutablePointer<(CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)>)

How could we change that to be InlineArray instead?

1 Like

This is discussed in the future directions of SE-0453.

1 Like

Good to know. Perhaps an ability to pass InlineArray (via & for a mutable version) similar to how we could pass String to char* API is on the cards.

Having said that, I found a simple workaround for now to avoid tuple:

  1. expose a "type erased" version to Obj-c bridging header:
int c_fillArray(void*);
  1. Obviously do not make that available to C, so it doesn't complain about it:
// C is still using this:
int c_fillArray(char arr[][10]) { ... }
  1. Optionally make a convenience initialiser:
extension String {
    init<let N: Int>(cString: InlineArray<N, CChar>) {
        var items = cString
        self = withUnsafeBytes(of: &items) { p in
            String(cString: p.baseAddress!.assumingMemoryBound(to: CChar.self))
        }
    }
}
  1. And finally this is the usage:
func manageCommand(cmdLine: String) -> Void {
    var cmd: [5 of [10 of CChar]] = .init(repeating: .init(repeating: 0))
    let number = withUnsafeMutableBytes(of: &cmd) { p in
        c_fillArray(p.baseAddress!) // note this is now type compatible
    }
    let strings = (0 ..< Int(number)).map { i in
        String(cString: cmd[i])
    }
    print(strings)
}

No tuple is needed!

This is now reasonably concise and clean.

:+1:It’s really better but how can I set the maximum string array element length (actually 10 chars) and the maximum array size (actually 5 elements)?

What do you mean by that?
Obviously you could hardcode different values:

var cmd: [100 of [64 of CChar]]

with the corresponding change to int c_fillArray(char arr[][64]) in the C API.

Or do you mean you want to do this dynamically from C?