5

I noticed some unusual behaviour when working with a C library which took strings in as const char * (which is converted to Swift as UnsafePointer<Int8>!); passing a String worked as expected, but a String? seemed to corrupt the input. Consider the test I wrote:

func test(_ input: UnsafePointer<UInt8>?) {
    if let string = input {
        print(string[0], string[1], string[2], string[3], string[4], string[5])
    } else {
        print("nil")
    }
}

let input: String = "Hello"

test(input)

This works as expected, printing a null-terminated list of UTF-8 bytes for the input string: 72 101 108 108 111 0

However, if I change the input to an optional string, so that it becomes:

let input: String? = "Hello"

I get a completely different set of values in the result (176 39 78 23 1 0), even though I would expect it to be the same. Passing in nil works as expected.

The C library's function allows NULL in place of a string, and I sometimes want to pass that in in Swift as well, so it makes sense for the input string to be an optional.

Is this a bug in Swift, or was Swift not designed to handle this case? Either way, what's the best way to handle this case?

Edit

It appears to have something to do with multiple arguments. The C function:

void multiString(const char *arg0, const char *arg1, const char *arg2, const char *arg3) {
    printf("%p: %c %c %c\n", arg0, arg0[0], arg0[1], arg0[2]);
    printf("%p: %c %c %c\n", arg1, arg1[0], arg1[1], arg1[2]);
    printf("%p: %c %c %c\n", arg2, arg2[0], arg2[1], arg2[2]);
    printf("%p: %c %c %c\n", arg3, arg3[0], arg3[1], arg3[2]);
}

Swift:

let input0: String? = "Zero"
let input1: String? = "One"
let input2: String? = "Two"
let input3: String? = "Three"

multiString(input0, input1, input2, input3)

Results in:

0x101003170: T h r
0x101003170: T h r
0x101003170: T h r
0x101003170: T h r

It appears that there's a bug with how Swift handles multiple arguments.

12
  • Here is a (perhaps related) thread about passing optional strings to C functions, with various workarounds for Swift 2: stackoverflow.com/questions/39357921/…. However, my observation at that time was that it is no problem with Swift 3 anymore. Commented Oct 1, 2016 at 14:26
  • 1
    I just tried it with a C function taking a const char * string without nullable annotation, and it worked correctly in Swift 3. I can reproduce the problem with your test code (and the output seems to be random). That is strange and I don't have an explanation yet. I just cannot reproduce the problem with a "real" C function imported to Swift 3. Commented Oct 1, 2016 at 14:39
  • 1
    Confirmed, I can reproduce that one. One can see that the same pointer is passed for all string arguments. You should file a bug report at bugs.swift.org ! Commented Oct 1, 2016 at 15:08
  • 1
    Done so, thanks for your help! In the meantime I guess I'll just make the Swift API more restrictive to avoid the optionals in the first place, as there doesn't seem to be a good workaround. Commented Oct 1, 2016 at 15:24
  • 1
    @SteveB: bugs.swift.org/browse/SR-2814 Commented Dec 22, 2016 at 13:54

3 Answers 3

1

I didn't find anything useful on if this is desired behaviour or just a bug.

The pragmatic solution would probably be to just have a proxy method like this, but you probably did something similar already.

func proxy(_ str: String?, _ functionToProxy: (UnsafePointer<UInt8>?) -> ()) {
    if let str = str {
        functionToProxy(str)
    } else {
        functionToProxy(nil)
    }
}

proxy(input, test)

Did you test if it was working in Swift 2? They changed something maybe related in Swift 3:

https://github.com/apple/swift-evolution/blob/master/proposals/0055-optional-unsafe-pointers.md

Sign up to request clarification or add additional context in comments.

2 Comments

Yeah, I tried Swift 2.2 on IBM's Swift Sandbox, but with the same result. I can't use the proxy function because the actual C function I'm using takes in five or six string arguments, any combination of which may be optional. Instead I'm trying to extend Optional (when it contains a String) to return a correct UnsafePointer (though I've not been successful so far). If I get it to work and no one else has submitted a better workaround or explanation, I'll post it as answer.
Looks like a bug when there are multiple optional arguments in a C function.
0

Just to be clear, there is a workaround until Apple fixes this. Unwrap your optional Strings before passing them and everything will work fine.

var anOptional: String?
var anotherOptional: String?

func mySwiftFunc() {

    let unwrappedA = anOptional!
    let unwrappedB = anotherOptional!

    myCStringFunc(unwrappedA, unwrappedB)

}

2 Comments

This doesn't actually work for my case, as I want to pass NULL into the C function if the optional String is nil (your answer would presumably cause a crash). Although I've not tested it, making a C function (in this case called unwrap) which simply takes an input string and returns it, and wrapping each argument in that, would probably work, looking something like: test_c_function(unwrap(optionalString1), unwrap(optionalString2), unwrap(optionalString3))
You could use the coalescing operator and check for an empty string instead of a null in your C function. That wouldn't work if an empty string was ever a valid input, though.
0

As mentioned in the comments, this is a clear bug in Swift.

Here's a workaround I'm using. If you can't trust Swift to convert the strings to pointers for you, then you've got to do it yourself.

Assuming C function defined as:

void multiString(const char *arg0, const char *arg1, const char *arg2);

Swift code:

func callCFunction(arg0: String?, arg1: String?, arg2: String?) {
    let dArg0 = arg0?.data(using: .utf8) as NSData?
    let pArg0 = dArg0?.bytes.assumingMemoryBound(to: Int8.self)

    let dArg1 = arg1?.data(using: .utf8) as NSData?
    let pArg1 = dArg1?.bytes.assumingMemoryBound(to: Int8.self)

    let dArg2 = arg2?.data(using: .utf8) as NSData?
    let pArg2 = dArg2?.bytes.assumingMemoryBound(to: Int8.self)

    multiString(pArg1, pArg2, pArg3)
}

Warning:

Don't be tempted to put this in a function like:

/* DO NOT USE -- BAD CODE */
func ocstr(_ str: String?) -> UnsafePointer<Int8>? {
    guard let str = str else {
        return nil
    }

    let nsd = str.data(using: .utf8)! as NSData

    //This pointer is invalid on return:
    return nsd.bytes.assumingMemoryBound(to: Int8.self)
}

which would remove repeated code. This doesn't work because the data object nsd gets deallocated at the end of the function. The pointer is therefore not valid on return.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.