Calling variadic C function from Swift with pointers

181 Views Asked by At

I'm trying to figure out how to call variadic C functions that write to pointers from Swift, such as vsscanf, but I don't understand how to actually construct the list of pointers to Swift variables.

I figure that if I have a string, I can get an UnsafePointer<CChar> and call vsscanf on it, but... how do I tell it where to actually write the data? How do I construct the CVaListPointer to pass to vsscanf?

var a: Int
var b: Float
"(5, 3.14)".withCString{buffer in
    let r = vsscanf(buffer, "(%d, %f)", /* how do I put a and b here? */)
}

Basically, doing the same thing as here (C):

#include <stdio.h>
#include <stdarg.h>

int parse(const char *buffer, char *format, ...)
{
    va_list args;
    va_start(args, format);
    int result = vsscanf(buffer, format, args);
    va_end(args);
    return result;
}

int main(int argc, char const *argv[])
{
    int a;
    float b;
    char s[] = "(5, 3.14)";

    int r = parse(s, "(%d, %f)", &a, &b);

    printf("a: %d, b: %f\n", a, b);
    // "a: 5, b: 3.140000"

    return 0;
}

UPDATE: Thanks to the answers so far, I feel like I understand how this works a bit better, but what I'm still struggling with is populating the CVaListPointer in an easier way. Here's an example of parsing a triplet of Doubles out of a string:

func parseVec3(s: String) -> (Double, Double, Double)? {
    var x: Double = 0
    var y: Double = 0
    var z: Double = 0
    
    let r = s.withCString { buffer in
        withUnsafeMutablePointer(to: &x) { ptrx in
            withUnsafeMutablePointer(to: &y) { ptry in
                withUnsafeMutablePointer(to: &z) { ptrz in
                    withVaList([ptrx, ptry, ptrz]) { va in
                        return vsscanf(buffer, "(%lf %lf %lf)", va)
                    }
                }
            }
        }
    }
    
    return r == 3 ? (x, y, z) : nil
}

if let v = parseVec3(s: "(1 2 3)") {
    print(v)
}

Now, this does work. But my problem is that I'm parsing a file where the bulk of the lines (thousands upon thousands of them) are six groups of triplets of numbers. The towering structure of withUnsafeMutablePointer would look downright ridiculous. I'm sure I could parse it using some more Swift-native approach (or just regex) but I was hoping to just use vsscanf because parsing this file in C is outrageously simple:

int main(int argc, char const *argv[])
{
    char s[] = "(1 2 3) (5 9 1) (0 5 8)";

    Vec3 a, b, c = {0, 0, 0};

    sscanf(s,
        "(%f %f %f) (%f %f %f) (%f %f %f)",
        &(a.x), &(a.y), &(a.z),
        &(b.x), &(b.y), &(b.z),
        &(c.x), &(c.y), &(c.z)
    );

    printf("a: (x: %f, y: %f, z: %f)\n", a.x, a.y, a.z);
    printf("b: (x: %f, y: %f, z: %f)\n", b.x, b.y, b.z);
    printf("c: (x: %f, y: %f, z: %f)\n", c.x, c.y, c.z);

    return 0;
}

Doing this with the withUnsafeMutablePointer approach in Swift as above would result in 11 with<Whatever> scopes, and that's only half of the floats parsed...

I figure I should be able to do something like this, but I can't figure out how to get the pointer offset to the other struct members:

func parseVec3_3(s: String) -> Vector3? {
    var output = Vector3(x: 0, y: 0, z: 0)
    
    var r: CInt = 0
    s.withCString { buffer in
        withUnsafeMutablePointer(to: &output) { ptr in
            withVaList([ptr, /* offset pointer here */]) { va in
                r = vsscanf(buffer, "(%lf %lf %lf)", va)
            }
        }
    }
    
    return r == 3 ? output : nil
}
2

There are 2 best solutions below

1
On

Like this:

var a: Int = 0
var b: Float = 0
withUnsafePointer(to: &a) { pointerToA in
    withUnsafePointer(to: &b) { pointerToB in
        withVaList([pointerToA, pointerToB]) { va_list in
            "(5, 3.14)".withCString { buffer in
                let r = vsscanf(buffer, "(%d, %f)", va_list)
            }
        }
    }
}
print(a)
print(b)

outputs

5
3.14

Update
The answer for OP edit including question about Vector3(x: 0, y: 0, z: 0) should become:

public struct Vector3: Equatable {
  public var x: CGFloat
  public var y: CGFloat
  public var z: CGFloat

  public init(x: CGFloat, y: CGFloat, z: CGFloat) {
    self.x = x
    self.y = y
    self.z = z
  }
}

func parseVec3_3(s: String) -> Vector3? {
    var output = Vector3(x: 0, y: 0, z: 0)
    var r: CInt = 0
    s.withCString { buffer in
        withUnsafePointer(to: &output) { (outputPointer: UnsafePointer<Vector3>) in
            outputPointer.withMemoryRebound(to: CGFloat.self, capacity: 3) {
                withVaList([OpaquePointer($0), OpaquePointer($0.advanced(by: 1)), OpaquePointer($0.advanced(by: 2))]) { va in
                    r = vsscanf(buffer, "(%lf %lf %lf)", va)
                }
            }
        }
    }
    return r == 3 ? output : nil
}

if let vector: Vector3 = parseVec3_3(s: "(1.0 2.0 3.0)") {
    print(vector)
}

outputs

Vector3(x: 1.0, y: 2.0, z: 3.0)
2
On

From the documentation on CVarArgs:

To create a wrapper for the c_api function, write a function that takes CVarArg arguments, and then call the imported C function using the withVaList(_:_:) function.

Swift only imports C variadic functions that use a va_list for their arguments. C functions that use the ... syntax for variadic arguments are not imported, and therefore can’t be called using CVarArg arguments.

Your wrapper function could look like:

func vsscanfSwiftWrapper(
    buffer: UnsafePointer<CChar>,
    format: UnsafePointer<CChar>,
    _ arguments: CVarArg...
) -> CInt {
    withVaList(arguments) { vaList in
        vsscanf(buffer, format, vaList)
    }
}