2

I am trying to call a C function (an extern API) from Swift with the following signature:

extern void k(int, const char*, ...)

I am calling the C function like this:

func send(query: String, args: CVarArg...) {
    let query2 = UnsafeMutablePointer(mutating: query.cString(using: String.defaultCStringEncoding))
    let result = k(0, query2, args)
}

But getting the error 'k' is unavailable: Variadic function is unavailable.

I am unsure if this is possible since the C API doesn't accept a va_list (http://swiftdoc.org/v3.1/protocol/CVarArg/). I do not mind having to write a wrapper function in C if need be.

too honest for this site
  • 12,050
  • 4
  • 30
  • 52
Morten
  • 1,819
  • 5
  • 28
  • 37

2 Answers2

6

You cannot call a C function taking a variable argument list from Swift. You'll need a variant taking a va_list parameter (similar to printf and vprintf):

int kv(int n, const char *s, va_list args) {
    // ...
}

The original function k can forward to kv:

int k(int n, const char* s, ...) {
    va_list args;
    va_start(args, s);
    int result = kv(n, s, args);
    va_end(args);
    return result;
}

Now kv can be called from Swift:

func send(query: String, args: CVarArg...) {
    let result = withVaList(args) {
        kv(0, query, $0)
    }
}

(You can pass a Swift String directly to a C function taking a const char * parameter, the compiler inserts the necessary code to create a temporary representation as null-terminated C string.)

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks, that was what I suspected. Unfortunately, as I am working with an external API I am not able to modify the original function with the ellipsis parameter. – Morten Jun 01 '17 at 13:16
4

Create a wrapper for every possible number of arguments:

void k1(int x, const char *param1) {
    k(x, param1);
}
void k2(int x, const char *param1, const char *param2) {
    k(x, param1, param2);
}
void k3(int x, const char *param1, const char *param2, const char *param3) {
    k(x, param1, param2, param3);
}

This is likely your only option but you can make it more generic...

We can create a function taking va_list, count the arguments and call the specific k1, k2, etc. variant, to provide a "nicer" API for Swift:

void genericK(int x, va_list params) {
    const char * paramArray[10];        
    int numParams = 0;

    NSString* arg = nil;
    while ((arg = va_arg(params, NSString *))) {
        paramArray[numParams++] = [arg cStringUsingEncoding:[NSString defaultCStringEncoding]];
    }

    switch (numParams) {
    case 1:
        k(x, paramArray[0], NULL);
        break;
    case 2:
        k(x, paramArray[0], paramArray[1], NULL);
        break;
    case 3:
        k(x, paramArray[0], paramArray[1], paramArray[2], NULL);
        break;
    default:
        assert(false);
    }
}

Then you can create another wrapper in Swift, that takes variable number of arguments again:

func k(_ x: Int32, _ params: String...) {
    withVaList(params) {
        genericK(x, $0)
    }
}

k(0, "testA", "testB", "testC")

The solution has one problem - you are limiting the number or arguments to a fixed number. That shouldn't be a big problem though...

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Unless I am mistaken, the argument list passed to `genericK` is not NULL-terminated. – Martin R May 29 '17 at 18:24
  • @MartinR I thought that too but I had troubles making it work without that `NULL`. – Sulthan May 29 '17 at 19:11
  • Passing the number of arguments would be an alternative. One *can* append NULL to the va_list but it is not very elegant: https://stackoverflow.com/a/32064355/1187415. – Martin R May 29 '17 at 19:13
  • @MartinR I am sure there are other possible improvements, my C is a bit rusty. I could make the function directly accept `NSArray`, for example. No need for `va_list`. – Sulthan May 29 '17 at 19:15