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.

Robert
  • 5,735
  • 3
  • 40
  • 53
  • Here is a (perhaps related) thread about passing optional strings to C functions, with various workarounds for Swift 2: http://stackoverflow.com/questions/39357921/why-cant-i-pass-an-optional-swift-string-to-c-function-that-allows-null-pointer. However, my observation at that time was that it is no problem with Swift 3 anymore. – Martin R Oct 01 '16 at 14:26
  • @MartinR: Perhaps it works when `_Nullable` is added in the C definition? I did consider that at first, but thought it was unlikely as in my test function, I used a regular optional rather than an implicitly unwrapped one, which I got when I imported the C library. Even if that's the case, I don't think it'll help because I can't modify the original library. – Robert Oct 01 '16 at 14:36
  • 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. – Martin R Oct 01 '16 at 14:39
  • That is odd! If you're interested, the specific C function I'm having a problem with is `mysql_real_connect` from libmysqlclient. – Robert Oct 01 '16 at 14:42
  • Well, I won't install MySQL to reproduce the problem :-) Perhaps it occurs with C functions taking multiple string parameters? – Martin R Oct 01 '16 at 14:48
  • Yeah, of course! I'll see if I can run it on a function which takes just a single string... – Robert Oct 01 '16 at 14:51
  • @MartinR: I think you might be right, I've updated my question with a C test case. – Robert Oct 01 '16 at 15:04
  • 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 https://bugs.swift.org ! – Martin R Oct 01 '16 at 15:08
  • 1
    [Done so](https://bugs.swift.org/browse/SR-2814), 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. – Robert Oct 01 '16 at 15:24
  • I just upgraded to XCode 8.2.1 and the bug is still there. Do you have a bug report number? I'd like to follow Apple's comments on it. – SteveB Dec 22 '16 at 04:07
  • I can also verify that if you unwrap the optionals before passing them, everything works fine. – SteveB Dec 22 '16 at 04:14
  • 1
    @SteveB: https://bugs.swift.org/browse/SR-2814 – Robert Dec 22 '16 at 13:54

3 Answers3

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

retendo
  • 1,309
  • 2
  • 12
  • 18
  • 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. – Robert Oct 01 '16 at 14:18
  • Looks like a bug when there are multiple optional arguments in a C function. – Etan Oct 05 '16 at 16:53
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)

}
SteveB
  • 483
  • 1
  • 4
  • 18
  • 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))` – Robert Dec 22 '16 at 13:58
  • 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. – SteveB Dec 22 '16 at 14:30
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.

HughHughTeotl
  • 5,439
  • 3
  • 34
  • 49