Pass string to C API which has va_list argument

Hi, may I ask why the first example works fine but the second one doesn't?

(Note that I hardcode vprintf() format string in both examples for simplicity).

Example 1 (it works):

import Foundation

func testCAPI1(arguments: CVarArg...) {
    return withVaList(arguments) { va_list in
        vprintf("%d\n", va_list)
    }
}

testCAPI1(arguments: 10)

Example 2 (not working):

import Foundation

func testCAPI2(arguments: CVarArg...) {
    return withVaList(arguments) { va_list in
        vprintf("%s\n", va_list)
    }
}

testCAPI2(arguments: "abc")

The second example caused EXC_BAD_ACCESS (code=EXC_I386_GPFLT) failure, which I suspect was related to invalid pointer. However, since the Swift string to C string (that is, char *) conversion is encapsulated in CVarArg, it seems I can't do much about it.

I find the following in this doc:

A Swift String can be used as a const char* pointer, which will pass the function a pointer to a null-terminated, UTF-8-encoded representation of the string. For instance, we can pass strings directly to standard C and POSIX library functions.

I think I did the same in the example 2. The only difference is that the C func gets the string through CVarArg. It's unlikely that this is not supported. I wonder what am I missing?

Background: I need to call sprintf() in swift code. I ran into the above issue when I tried to get the sample code in Apple doc working. The sample code doesn't work with String parameters also.

Thanks for any help.

A String will be auto-converted to a char * if that's the declared type of the function, but for a variadic argument list it's converted to an NSString instead (for use with %@). You can use withCString to get a C string pointer you can pass as a CVarArg, though note that the pointer is only valid inside the callback.

2 Likes

@jrose Thanks for the explanation. However, I doubt if the approach you suggested is practical in this case. Let's see the example code from Apple doc (that's the code I'm trying to get working).

func swiftprintf(format: String, arguments: CVarArg...) -> String? {
    return withVaList(arguments) { va_list in
        var buffer: UnsafeMutablePointer<Int8>? = nil
        return format.withCString { cString in
            guard vasprintf(&buffer, cString, va_list) != 0 else {
                return nil
            }
 
            return String(validatingUTF8: buffer!)
        }
    }
}

If I understand it correctly, to use your approach, I need to go through each argument in CVarArg and calls withCString on the argument if it's a string, right? That seems impossible to implement, I'm afraid. Please correct me if I misunderstand it. Thanks.

I think what you meant is something like:

"abc".withCString { cString in
    testCAPI2(arguments: cString)
}

It works in this specific simple test. But in general we'll need to go through CVarArg and generate nested calls, which I doubt if it's possible to implement.

1 Like

I worked out a solution. The idea is like what @jrose said, but to implement it I have to do recursive calls which mix up closure and function calls. It looks like a crazy solution to a simple problem.

(For simplicity the code assumes all parameters are of String type.)

func processArgs(format: UnsafePointer<Int8>, arguments: inout [String], cStrings: inout [UnsafePointer<Int8>], buffer: inout UnsafeMutablePointer<Int8>?) -> Int32 {
    guard arguments.count > 0 else { fatalError("Internal Error") }
    
    return arguments[0].withCString { cString -> Int32 in
        arguments.remove(at: 0)
        cStrings.append(cString)
        if arguments.count == 0 {
            return withVaList(cStrings) { va_list -> Int32 in
                return vasprintf(&buffer, format, va_list)
            }
        } else {
            return processArgs(format: format, arguments: &arguments, cStrings: &cStrings, buffer: &buffer)
        }
    }
}

func sprintf(format: String, arguments: String...) -> String? {
    var arguments = arguments
    var cStrings: [UnsafePointer<Int8>] = []
    var buffer: UnsafeMutablePointer<Int8>? = nil
    
    guard arguments.count > 0 else { return format }
    
    return format.withCString { cString in
        guard processArgs(format: cString, arguments: &arguments, cStrings: &cStrings, buffer: &buffer) != 0 else {
            return nil
        }
        
        return String(validatingUTF8: buffer!)
    }
}

print(sprintf(format: "%s", arguments: "abc")!)
print(sprintf(format: "%s %s", arguments: "abc", "efg")!)
// This is the feature I need from sprintf()!
print(sprintf(format: "%2$s", arguments: "abc", "efg")!)

It works. But I'm not sure if I should use it because it's too tricky. Perhaps it would be better to write a custom version of String(format:) since my requirement is rather simple (just to support "%@" and "n$"). Anyway it's good to find a working solution :slight_smile: