Является ли Swift обработкой CVarArg для String ошибкой? - PullRequest
3 голосов
/ 14 июля 2020

При написании оболочки Swift для оболочки C библиотеки C ++ я наткнулся на несколько странных ошибок, касающихся CVarArg Swift. Оболочка C, которую я уже использую, использует функции Variadi c, которые я преобразовал в функции, используя va_list в качестве аргумента, чтобы их можно было импортировать (поскольку Swift не может импортировать функции C Variadi c). При передаче аргументов такой функции после подключения к Swift она использует частное свойство _cVarArgEncoding типов, соответствующих CVarArg, для «кодирования» значений, которые затем отправляются как указатель на функцию C. Однако кажется, что эта кодировка неверна для Swift String s.

Чтобы продемонстрировать, я создал следующий пакет:

Package.swift

// swift-tools-version:5.2

import PackageDescription

let package = Package(
    name: "CVarArgTest",
    products: [
        .executable(
            name: "CVarArgTest",
            targets: ["CVarArgTest"]),
    ],
    targets: [
        .target(
            name: "CLib"),
        .target(
            name: "CVarArgTest",
            dependencies: ["CLib"])
    ]
)

CLib

CTest.h

#ifndef CTest_h
#define CTest_h

#include <stdio.h>

/// Prints out the strings provided in args
/// @param num The number of strings in `args`
/// @param args A `va_list` of strings
void test_va_arg_str(int num, va_list args);

/// Prints out the integers provided in args
/// @param num The number of integers in `args`
/// @param args A `va_list` of integers
void test_va_arg_int(int num, va_list args);

/// Just prints the string
/// @param str The string
void test_str_print(const char * str);

#endif /* CTest_h */


CTest. c

#include "CTest.h"
#include <stdarg.h>

void test_va_arg_str(int num, va_list args)
{
    printf("Printing %i strings...\n", num);
    for (int i = 0; i < num; i++) {
        const char * str = va_arg(args, const char *);
        puts(str);
    }
}

void test_va_arg_int(int num, va_list args)
{
    printf("Printing %i integers...\n", num);
    for (int i = 0; i < num; i++) {
        int foo = va_arg(args, int);
        printf("%i\n", foo);
    }
}

void test_str_print(const char * str)
{
    puts(str);
}

main.swift

import Foundation
import CLib

// The literal String is perfectly bridged to the CChar pointer expected by the function
test_str_print("Hello, World!")

// Prints the integers as expected
let argsInt: [CVarArg] = [123, 456, 789]
withVaList(argsInt) { listPtr in
    test_va_arg_int(Int32(argsInt.count), listPtr)
}

// ERROR: Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
let argsStr: [CVarArg] = ["Test", "Testing", "The test"]
withVaList(argsStr) { listPtr in
    test_va_arg_str(Int32(argsStr.count), listPtr)
}

Пакет доступен здесь также .

Как указано в приведенном выше коде, печать String через C или va_list, содержащих Int s, работает должным образом, но при преобразовании в const char *, есть исключение (EXC_BAD_ACCESS (code=EXC_I386_GPFLT)).

Итак, вкратце: я испортил сторону C или Swift здесь что-то не так? Я тестировал это в Xcode 11.5 и 12.0b2. Если это ошибка, я буду рад сообщить об этом.

1 Ответ

2 голосов
/ 14 июля 2020

Это немного сложно: ваша строка на самом деле связана с Objective- C NSString *, а не C char *:

(lldb) p str
(const char *) $0 = 0x3cbe9f4c5d32b745 ""
(lldb) p (id)str
(NSTaggedPointerString *) $1 = 0x3cbe9f4c5d32b745 @"Test"

(если вы интересно, почему это NSTaggedPointerString, а не просто NSString, эта статья - отличное чтение - короче говоря, строка достаточно короткая, чтобы храниться непосредственно в байтах переменной-указателя а не в объекте в куче.

Глядя на исходный код для withVaList, мы видим, что представление va_list типа определяется его реализацией Свойство _cVarArgEncoding протокола CVarArg. Стандартная библиотека имеет некоторые реализации этого протокола для некоторых базовых c целочисленных и указательных типов , но для String нет ничего Итак, кто конвертирует нашу строку в NSString?

Поиск по репозиторию Swift на GitHub, мы обнаруживаем, что виновником является Foundation :

//===----------------------------------------------------------------------===//
// CVarArg for bridged types
//===----------------------------------------------------------------------===//
extension CVarArg where Self: _ObjectiveCBridgeable {
  /// Default implementation for bridgeable types.
  public var _cVarArgEncoding: [Int] {
    let object = self._bridgeToObjectiveC()
    _autorelease(object)
    return _encodeBitsAsWords(object)
  }
}

На простом английском sh: любой объект, который можно связать с Objecti. ve- C кодируется как vararg путем преобразования в объект Objective- C и кодирования указателя на этот объект. C varargs не являются типобезопасными, поэтому ваш test_va_arg_str просто предполагает, что это char*, и передает его puts, что дает сбой.

Так это ошибка? Я так не думаю - я полагаю, что такое поведение, вероятно, преднамеренно для совместимости с такими функциями, как NSLog, которые чаще используются с объектами Objective- C, чем с C. Тем не менее, это, безусловно, удивительная ловушка и, вероятно, одна из причин, по которой Swift не любит позволять вам вызывать C варианты c функции.

Вы захотите обойти это, вручную преобразовав ваши строки в C -строки. Это может показаться немного уродливым, если у вас есть массив строк, которые вы хотите преобразовать, не делая ненужных копий, но вот функция, которая должна уметь это делать.

extension Collection where Element == String {
    /// Converts an array of strings to an array of C strings, without copying.
    func withCStrings<R>(_ body: ([UnsafePointer<CChar>]) throws -> R) rethrows -> R {
        return try withCStrings(head: [], body: body)
    }
    
    // Recursively call withCString on each of the strings.
    private func withCStrings<R>(head: [UnsafePointer<CChar>],
                                 body: ([UnsafePointer<CChar>]) throws -> R) rethrows -> R {
        if let next = self.first {
            // Get a C string, add it to the result array, and recurse on the remainder of the collection
            return try next.withCString { cString in
                var head = head
                head.append(cString)
                return try dropFirst().withCStrings(head: head, body: body)
            }
        } else {
            // Base case: no more strings; call the body closure with the array we've built
            return try body(head)
        }
    }
}

func withVaListOfCStrings<R>(_ args: [String], body: (CVaListPointer) -> R) -> R {
    return args.withCStrings { cStrings in
        withVaList(cStrings, body)
    }
}

let argsStr: [String] = ["Test", "Testing", "The test"]
withVaListOfCStrings(argsStr) { listPtr in
    test_va_arg_str(Int32(argsStr.count), listPtr)
}

// Output:
// Printing 3 strings...
// Test
// Testing
// The test
...