Converting a C char array to a String
Asked Answered
B

9

34

I have a Swift program that does interop with a C library. This C library returns a structure with a char[] array inside, like this:

struct record
{
    char name[8];
};

The definition is correctly imported into Swift. However, the field is interpreted as a tuple of 8 Int8 elements (typed (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)), which I have no idea how to transform into a String with Swift.

There is no String initializer that accepts an Int8 tuple, and it doesn't seem possible to get a pointer to the first element of the tuple (since types can be heterogenous, that's not really surprising).

Right now, my best idea is to create a tiny C function that accepts a pointer to the structure itself and return name as a char* pointer instead of an array, and go with that.

Is there, however, are pure Swift way to do it?

Beaverette answered 13/12, 2014 at 5:11 Comment(8)
Are you sure interop makes it a C question? Or that your workaround does so? Especially as you want a pure-swift solution...Koons
@Deduplicator, if I was looking for how to convert a C char array to a Swift string, I'd look for tags "c" and "swift" for sure.Beaverette
There's nothing C about that byte-array but you having a description of it valid in C, C++, objective-C, objective-C++ and so on. Does not make it a C question.Koons
I know no one who refers to these as "C++ arrays" or "Objective-C arrays" or "Objective-C++ arrays", and I know no other definition of "C array". When I looked for a solution, I used "C array" in my search terms, and unless I'm an outlier, I believe the next person with the same problem will do the same. I think that tags are the most important for search requests, and that their categorization purpose comes second to that.Beaverette
If anything often described by using C as a synonym for low-level and native was tagged C, that would swamp the C tag with just about everything having to do with native interop. Very bad idea.Koons
Take this to meta if you want.Beaverette
Take a look at this article on using legacy C APIs, toward the end it talks about your issue: sitepoint.com/using-legacy-c-apis-swiftHeathenry
This issue comes up often in SO. The question is are tags really tags, or are they channels? If they are subject tags, then this belongs to both C & Swift search terms. If users subscribe to tags like channels, then they get spammed with things they aren't interested in... if that is the case, tags are overloaded and a SO implementation flaw.Disability
D
41

The C array char name[8] is imported to Swift as a tuple:

(Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)

The address of name is the same as the address of name[0], and Swift preserves the memory layout of structures imported from C, as confirmed by Apple engineer Joe Groff:

... You can leave the struct defined in C and import it into Swift. Swift will respect C's layout.

As a consequence, we can pass the address of record.name, converted to an UInt8 pointer, to the String initializer. The following code has been updated for Swift 4.2 and later:

let record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(to: record.name) {
    $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: $0)) {
        String(cString: $0)
    }
}

NOTE: It is assumed that the bytes in name[] are a valid NUL-terminated UTF-8 sequence.

For older versions of Swift:

// Swift 2:
var record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(&record.name) {
    String.fromCString(UnsafePointer($0))!
}

// Swift 3:
var record = someFunctionReturningAStructRecord()
let name = withUnsafePointer(to: &record.name) {
    $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size(ofValue: record.name)) {
        String(cString: $0)
    }
}
Deckard answered 13/12, 2014 at 6:32 Comment(9)
Yes, this works. I'll just add that record needs to be mutable (declared with var) or the Swift compiler shoots a weird error.Beaverette
@zneak: You are right, I have added that information to the answer.Deckard
I believe this will work, but is it documented that tuples are contiguous in memory? With arrays, you don't get that assurance unless you call withUnsafeBufferPointer, but tuple implementation hasn't been documented as far as I can tell.Noneffective
@NateCook: That is a good question. It seems plausible to me because the tuple is (in this particular case) the Swift view of a C array char name[8].Deckard
Normally when C returns a fixed-length string it is NOT guaranteed to be null-terminated—this code will probably not work if if 'name' has 8 characters.Curtiscurtiss
@WilShipley: Sure, that's why I added the last sentence to the answer.Deckard
@NateCook: Actually it is guaranteed if the structure is imported from C. I have updated the answer accordingly with a reference, and simplified the code.Deckard
Don't reference record.name in the MemoryLayout.size(..) call -- use $0 to avoid simultaneous access.Orva
@MechEthan: Thanks for the notice! There is another solution because (as of Swift 4.2) you can take the address of an immutable value, i.e. you can use withUnsafePointer(to:) with a constant value, and that avoids the problem of simultaneous (mutating) access as well. I have updated the code accordingly.Deckard
N
4

You can actually collect a tuple into an array by using Swift's variadic parameter syntax:

let record = getRecord()
let (int8s: Int8...) = myRecord          // int8s is an [Int8]
let uint8s = int8s.map { UInt8($0) }
let string = String(bytes: uint8s, encoding: NSASCIIStringEncoding)
// myString == Optional("12345678")
Noneffective answered 13/12, 2014 at 6:12 Comment(6)
That looks good (didn't know about the variadic tuple syntax), though in the real case, the tuple has 32 elements, and I could see myself needing that for larger arrays (like 128 elements). That would make for an annoyingly large type annotation. Do you see a way to make it independent of the number of elements?Beaverette
What construct is let (int8s: Int8...) ? Is let (name : type) = exp generally the same as let name : type = expr ?Deckard
@Beaverette In that case you'd need to do it inline, since with a function you'll have to type out the right number of elements to match each specific tuple arity. I've modified my answer to show what you can do.Noneffective
@MartinR The name for it is tuple decomposition. You can pull things out in interesting ways using this technique, try: let (_, second, _, fourth, theRest: Int...) = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)Noneffective
@NateCook: Thanks! I could not find the variadic part of tuple assignments in the documentation yet, but I will dig further. (That almost looks like Perl: my ($a, undef, @b) = (1, 2, 3, 4, 5, 6); :)Deckard
As of Swift 2 in the Xcode 7b1 beta, this no longer works.Beaverette
W
3

I'm interested in working this out for my own purposes as well, so I added a new function:

func asciiCArrayToSwiftString(cString:Int8...) -> String
{
    var swiftString = String()            // The Swift String to be Returned is Intialized to an Empty String
    var workingCharacter:UnicodeScalar = UnicodeScalar(UInt8(cString[0]))
    var count:Int = cString.count

    for var i:Int = 0; i < count; i++
    {
        workingCharacter = UnicodeScalar(UInt8(cString[i])) // Convert the Int8 Character to a Unicode Scalar
        swiftString.append(workingCharacter)             // Append the Unicode Scalar

    }

    return swiftString                     // Return the Swift String
}

I call this function with:

    let t:Int8 = Int8(116)
    let e:Int8 = Int8(101)
    let s:Int8 = Int8(115)
    let testCString = (t, e, s, t)
    let testSwiftString = wispStringConverter.asciiCArrayToSwiftString(testCString.0, testCString.1, testCString.2, testCString.3)
    println("testSwiftString = \(testSwiftString)")

the resulting output is:

testSwiftString = test

Wiesbaden answered 13/12, 2014 at 6:59 Comment(0)
D
2

I have just experienced a similar issue using Swift 3. (3.0.2). I was attempting to convert an Array of CChar, [CChar] to a String in Swift. It turns out Swift 3 has a String initializer which will take a cString.

Example:

let a = "abc".cString(using: .utf8) // type of a is [CChar]
let b = String(cString: a!, encoding: .utf8) // type of b is String
print("a = \(a)")
print("b = \(b)")

results in

a = Optional([97, 98, 99, 0])

b = Optional("abc")

Note that the cString function on String results in an Optional. It must be force unwrapped when used in the String.init function creating b. And b is also Optional... meaning both could end up being nil, so error checking should also be used.

Distressful answered 23/3, 2017 at 18:51 Comment(0)
W
1

Try this:

func asciiCStringToSwiftString(cString:UnsafePointer<UInt8>, maxLength:Int) -> String
{
    var swiftString = String()  // The Swift String to be Returned is Intialized to an Empty String
    var workingCharacter:UnicodeScalar = UnicodeScalar(cString[0])
    var count:Int = 0           // An Index Into the C String Array Starting With the First Character

    while cString[count] != 0             // While We Haven't reached the End of the String
    {
        workingCharacter = UnicodeScalar(cString[count]) // Convert the ASCII Character to a Unicode Scalar
        swiftString.append(workingCharacter)             // Append the Unicode Scalar Version of the ASCII Character
        count++                                          // Increment the Index to Look at the Next ASCII Character

        if count > maxLength                            // Set a Limit In Case the C string was Not NULL Terminated
        {
            if printDebugLogs == true
            {
                swiftString="Reached String Length Limit in Converting ASCII C String To Swift String"
            }
            return swiftString
        }
    }

    return swiftString                     // Return the Swift String
}
Wiesbaden answered 13/12, 2014 at 5:34 Comment(3)
The C string is not an UnsafePointer<UInt8>, it's a tuple of 8 Int8 elements, so this method does not solve my problem. Besides, the String class has an init?(UTF8String: UnsafePointer<CChar>) failable initializer that does this exactly.Beaverette
In my application I need to log the current OpenGL version to be sure the pixel format was set correctly. I do this with let glVersionCString:UnsafePointer<UInt8> = glGetString(GLenum(GL_VERSION)). The compiler will not let me cast the return from glGetString as an UnsafePointer<CChar>, so I can't use the Swift String initializer. That's the reason for this function.Wiesbaden
I'm not questioning the usefulness of your function in your specific use case, however it does not apply here. This works for you because glGetString returns a pointer. The struct I deal with has an array field, which is very different from a pointer field. As I said, the field's type as seen by Swift is (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8), not UnsafePointer<UInt8>.Beaverette
A
1

Here's a solution I came up with which uses reflection to actually convert the tuple into an [Int8] (see Any way to iterate a tuple in swift?), and then converts it to a string using fromCString...() methods.

func arrayForTuple<T,E>(tuple:T) -> [E] {
    let reflection = reflect(tuple)
    var arr : [E] = []
    for i in 0..<reflection.count {
        if let value = reflection[i].1.value as? E {
            arr.append(value)
        }
    }
    return arr
}

public extension String {
    public static func fromTuple<T>(tuple:T) -> String? {
        var charArray = arrayForTuple(tuple) as [Int8]
        var nameString = String.fromCString(UnsafePointer<CChar>(charArray))
        if nameString == nil {
            nameString = String.fromCStringRepairingIllFormedUTF8(UnsafePointer<CChar>(charArray)).0
        }
        return nameString
    }
}
Amoebaean answered 20/3, 2015 at 17:36 Comment(0)
B
1

Swift 3. Only uses reflection. This version stops building the string when it encounters a null byte. Tested.

func TupleOfInt8sToString( _ tupleOfInt8s:Any ) -> String? {
    var result:String? = nil
    let mirror = Mirror(reflecting: tupleOfInt8s)

    for child in mirror.children {
        guard let characterValue = child.value as? Int8, characterValue != 0 else {
            break
        }

        if result == nil {
            result = String()
        }
        result?.append(Character(UnicodeScalar(UInt8(characterValue))))
    }

    return result
}
Bussard answered 1/11, 2016 at 2:6 Comment(0)
S
1

Details

  • Xcode 11.2.1 (11B500), Swift 5.1

Solution

extension String {
    init?(fromTuple value: Any) {
        guard let string = Tuple(value).toString() else { return nil }
        self = string
    }

    init?(cString: UnsafeMutablePointer<Int8>?) {
        guard let cString = cString else { return nil }
        self = String(cString: cString)
    }

    init?(cString: UnsafeMutablePointer<CUnsignedChar>?) {
        guard let cString = cString else { return nil }
        self = String(cString: cString)
    }

    init? (cString: Any) {

        if let pointer = cString as? UnsafeMutablePointer<CChar> {
            self = String(cString: pointer)
            return
        }

        if let pointer = cString as? UnsafeMutablePointer<CUnsignedChar> {
            self = String(cString: pointer)
            return
        }

        if let string = String(fromTuple: cString) {
            self = string
            return
        }

        return nil
    }
}

// https://mcmap.net/q/336006/-any-way-to-iterate-a-tuple-in-swift

struct Tuple<T> {
    let original: T
    private let array: [Mirror.Child]
    init(_ value: T) {
        self.original = value
        array = Array(Mirror(reflecting: original).children)
    }
    func compactMap<V>(_ transform: (Mirror.Child) -> V?) -> [V] { array.compactMap(transform) }

    func toString() -> String? {

        let chars = compactMap { (_, value) -> String? in
            var scalar: Unicode.Scalar!
            switch value {
            case is CUnsignedChar: scalar = .init(value as! CUnsignedChar)
            case is CChar: scalar = .init(UInt8(value as! CChar))
            default: break
            }
            guard let _scalar = scalar else { return nil }
            return String(_scalar)
        }
        if chars.isEmpty && !array.isEmpty { return nil }
        return chars.joined()
    }
}

Usage (full sample)

Code in C language (Header.h)

#ifndef Header_h
#define Header_h

#ifdef __cplusplus
extern "C" {
#endif

char c_str1[] = "Hello world!";
char c_str2[50] = "Hello world!";
char *c_str3 = c_str2;

typedef unsigned char UTF8CHAR;
UTF8CHAR c_str4[] = {72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 0};
UTF8CHAR *c_str5 = c_str4;
UTF8CHAR c_str6[] = {'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\0'};
UTF8CHAR *c_str7 = 0;
UTF8CHAR *c_str8 = "";

#define UI BYTE

#ifdef __cplusplus
}
#endif

#endif /* Header_h */

...-Bridging-Header.h

#include "Header.h"

Swift code

func test() {
    printInfo(c_str1)
    printInfo(c_str2)
    printInfo(c_str3)
    printInfo(c_str4)
    printInfo(c_str5)
    printInfo(c_str6)
    printInfo(c_str7)
    printInfo(c_str8)

    print(String(fromTuple: c_str1) as Any)
    print(String(fromTuple: c_str2) as Any)
    print(String(cString: c_str3) as Any)
    print(String(fromTuple: c_str4) as Any)
    print(String(cString: c_str5) as Any)
    print(String(fromTuple: c_str6) as Any)
    print(String(fromTuple: c_str7) as Any)
    print(String(cString: c_str8) as Any)
}

var counter = 1;

func printInfo(_ value: Any?) {
    print("name: str_\(counter)")
    counter += 1
    guard let value = value else { return }
    print("type: \(type(of: value))")
    print("value: \(value)")
    print("swift string: \(String(cString: value))")
    print("\n-----------------")
}

Output

name: str_1
type: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0)
swift string: Optional("Hello world!\0")

-----------------
name: str_2
type: (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 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, 0, 0, 0, 0, 0, 0)
swift string: Optional("Hello world!\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\0\0\0\0\0\0")

-----------------
name: str_3
type: UnsafeMutablePointer<Int8>
value: 0x000000010e8c5d40
swift string: Optional("Hello world!")

-----------------
name: str_4
type: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 32, 0)
swift string: Optional("Hello world \0")

-----------------
name: str_5
type: UnsafeMutablePointer<UInt8>
value: 0x000000010e8c5d80
swift string: Optional("Hello world ")

-----------------
name: str_6
type: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
value: (72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 0)
swift string: Optional("Hello world!\0")

-----------------
name: str_7
name: str_8
type: UnsafeMutablePointer<UInt8>
value: 0x000000010e8c0ae0
swift string: Optional("")

-----------------
Optional("Hello world!\0")
Optional("Hello world!\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\0\0\0\0\0\0")
Optional("Hello world!")
Optional("Hello world \0")
Optional("Hello world ")
Optional("Hello world!\0")
Optional("")
Optional("")
Swart answered 15/11, 2019 at 18:4 Comment(0)
G
1

There have been multiple answers already on this topic, but not a single one is a simple one line nor do they address non null terminated ones.

Assuming the String is NULL terminated:

struct record {
    char name[8];
};

//Might by unsafe, depends
String(cString: &record.name.0)

//Safe
String(cString: unsafeBitCast(UnsafePointer(&record.name), to: UnsafePointer<Int8>.self))

For Strings that aren't NULL terminated:

//Might by unsafe, depends
String(cString: &record.name.0).prefix(MemoryLayout.size(ofValue: record.name))

//Safe
String(bytesNoCopy: UnsafeMutableRawPointer(mutating: &record.name), length: MemoryLayout.size(ofValue: record.name), encoding: .utf8, freeWhenDone: false)

––––

Regarding @MartinR concern about passing just one byte, you could also pass a pointer to the entire variable too, but personally, I've never experienced swift just passing one byte, so it should be safe.

Gainless answered 29/3, 2020 at 14:30 Comment(4)
Note that there are two aspects of undefined behaviour in your solution. First, you pass only a single character as inout expression to the String initialiser, compare OOP's comment here: https://mcmap.net/q/412351/-get-data-from-uuid-in-swift-3. Second, if the string is not NULL terminated then you might read the contents of undefined memory.Deckard
@MartinR Then how about passing a ref to the entire variable?Gainless
That should work, and most probably there is no runtime difference. I still prefer withMemoryRebound over unsafeBitCast, which is documented as “Use this function only to convert the instance passed as x to a layout-compatible type when conversion through other means is not possible. ... Calling this function breaks the guarantees of the Swift type system; use with extreme care.”Deckard
@MartinR Regarding the not null terminated string, yes indeed, people should use my method with caution, in some cases it might be dangerous, but in others, like mine, its perfectly safe. I had a struct with two char[] and then some other stuff. Between the arrays and other stuff was a padding and the struct was 0'ed before filled, so its guaranteed that it will stop reading there and won‘t read non alloc'ed memory.Gainless

© 2022 - 2024 — McMap. All rights reserved.