преобразовать std::string в строку Swift

Краткая версия:

как я могу преобразовать std::string (объект, возвращаемый функцией .cpp, вызванной из файла .Swift с использованием моста) в Swift String?

Длинная версия:

У меня есть библиотека, написанная на C++, и мне нужно вызвать некоторый код с помощью Swift. Я создал мост, добавив два файла в свои проекты Xcode:

связующий заголовок, который позволяет Swift вызывать функции C (на самом деле Swift не может напрямую вызывать функции C++, поэтому ему необходимо проходить через функции C)

//file bridgingHeader.h
const char * getLastOpenedFile();

и файлы .cpp, которые могут вызывать (конечно) функции C++ внутри и могут определять функции C с помощью extern "C"

//file wrapper.cpp    
#include <string>
#include "my_library.hpp"

extern "C" const char * getStringFromLibrary()
{
    const char * s = get_string_from_library().c_str();
    return s;
}

Я могу получить доступ к возвращаемому значению из файла .swift, используя

let myString = String(cString: getStringFromLibrary())
Swift.print(myString)

Поставив точку останова для проверки значения s внутри функции getStringFromLibrary() я вижу содержимое строки, значит функция из библиотеки вызывается корректно.

В любом случае файл .swift печатает какие-то странные символы, а не исходную строку. Изменение getStringFromLibrary() следующим образом

extern "C" const char * getStringFromLibrary()
{        
    return get_string_from_library().c_str();
}

Как следствие, код .swift печатает префикс реальной строки. Это заставляет меня задуматься о проблеме с памятью: вероятно, когда getStringFromLibrary() выходит, объект std::string, возвращенный get_string_from_library(), уничтожается, и поэтому память, на которую указывает указатель, возвращенный с помощью .c_str(), больше не является надежной, поэтому я получаю неверные результаты от Swift.print().

Как правильно получить доступ к std::string из файла .swift, а затем освободить его память?


person Nisba    schedule 12.04.2018    source источник
comment
Ваш getStringFromLibrary будет висеть на оборванном указателе, поскольку s ссылается на const char*, который был выделен и уничтожен функцией get_string_from_library. Я не знаком с быстрым, но вам, по сути, придется выделить этот const char* и передать право собственности вызывающему.   -  person Cory Kramer    schedule 13.04.2018
comment
возможно, решение состоит в том, чтобы вызвать malloc внутри getStringFromLibrary() для выделения памяти для переменной char *, копирования содержимого переменной в строку Swift и последующего явного вызова free()   -  person Nisba    schedule 13.04.2018
comment
@Nisba Да, это обычный способ. Вы можете рассмотреть возможность добавления функции freeStringFromLibrary вместо вызова free непосредственно из Swift, чтобы скрыть основной метод выделения памяти. Это позволит вам использовать new[] или malloc внутри.   -  person Miles Budnek    schedule 13.04.2018


Ответы (4)


Вы можете написать оболочку Objective-C++ для работы с кодами C++.

соединительный заголовок:

#include "Wrapper.hpp"

Обертка.hpp:

#ifndef Wrapper_hpp
#define Wrapper_hpp

#import <Foundation/Foundation.h>

#if defined(__cplusplus)
extern "C" {
#endif
    NSString * _Nonnull getStringFromLibrary();
#if defined(__cplusplus)
}
#endif

#endif /* Wrapper_hpp */

Обертка.мм:

#include "Wrapper.hpp"

#include <string>
#include "my_library.hpp"

NSString * _Nonnull getStringFromLibrary() {
    return [NSString stringWithUTF8String:get_string_from_library().c_str()];
}

Свифт-код:

print(getStringFromLibrary())

[NSString stringWithUTF8String:] копирует содержимое буфера во внутреннее хранилище, и ARC успевает его освободить. Вам не нужно определять free_something().

person OOPer    schedule 12.04.2018

Объект std::string владеет буфером, на который возвращается указатель через c_str. Это означает, что getStringFromLibrary возвращает указатель на ничто.

Есть несколько способов избежать этого:

1) Копируем содержимое буфера куда-нибудь долгоживущее и возвращаем туда указатель. Обычно это означает выделение памяти через new[] или malloc:

extern "C"
{

const char* getStringFromLibrary()
{
    std::string str = get_string_from_library();
    char* s = new char[str.size() + 1]{};
    std::copy(str.begin(), str.end(), s);
    return s;
}

void freeStringFromLibrary(char* s)
{
    delete[] s;
}

}

Этот метод прост, но требует затрат на дополнительную копию. Это может иметь значение или не иметь значения в зависимости от того, насколько велик str и как часто вызывается getStringFromLibrary. Это также требует, чтобы пользователь имел дело с управлением ресурсами, поэтому это не особенно безопасно для исключений.

2) Верните непрозрачный «дескриптор» объекту std::string и дайте быстрому доступу к его базовому буферу и освободите его с помощью различных функций:

extern "C"
{

void* getStringFromLibrary()
{
    std::string* s = new std::string(get_string_from_library());
    return s;
}

const char* libraryGetCString(void* s)
{
    return static_cast<std::string*>(s)->c_str();
}

void freeStringFromLibrary(void* s)
{
    delete static_cast<std::string*>(s);
}

}

Теперь в Swift вы можете сделать что-то вроде этого:

let handle: UnsafeMutablePointer<COpaquePointer> = getStringFromLibrary()
let myString = String(cString: libraryGetCString(handle))
freeStringFromLibrary(handle)

Этот подход устраняет лишнюю копию, но все еще не является безопасным для исключений, поскольку вызывающая сторона должна иметь дело с управлением ресурсами.

Вероятно, есть способ смешать Objective-C++ с решением, чтобы избежать проблем с управлением ресурсами, но, к сожалению, я недостаточно знаком со Swift или Objective-C++, чтобы рассказать вам, как выглядит это решение.

person Miles Budnek    schedule 12.04.2018
comment
Разве дополнительная копия не скрыта в std::string* s = new std::string(get_string_from_library());? - person Nisba; 13.04.2018
comment
Нет, строка, возвращаемая get_string_from_library, будет перемещена в новый объект string. Для этого требуется только копия указателя вместо копирования всей строки. - person Miles Budnek; 13.04.2018
comment
собираемся для очков стиля с первым подходом: используйте strdup() и free() вместо new/copy/delete[]. - person Alex Cohn; 13.12.2020

Я решил, я публикую решение, с которым я столкнулся.

В будущем я, вероятно, напишу класс, который позволит мне управлять операцией удаления автоматически и более безопасно. Я обновлю ответ.

соединительный заголовок:

//file bridgingHeader.h
char * getStringFromLibrary();
void freeString(char * string);

обертка:

char * std_string_to_c_string(std::string s0) {
    size_t length = s0.length() + 1;
    char * s1 = new char [length];
    std::strcpy(s1, s0.c_str());
    return s1;
}

extern "C" void freeString(char * string) {    
    delete [] string;
}

extern "C" char * getStringFromLibrary()
{
    return std_string_to_c_string(get_string_from_library().c_str());
}

Свифт-код:

let c_string: UnsafeMutablePointer<Int8>! = getStringFromLibrary()
let myString = String(cString: c_string)
freeString(c_string)

Swift.print(myString)
person Nisba    schedule 12.04.2018

Я определяю функцию (это может быть статическое расширение String, если хотите):

func getStringAndFree(_ cString: UnsafePointer<Int8>) -> String {
    let res = String(cString: cString)
    cString.deallocate()
    return res
}

а в С++,

extern "C" char *getStringFromLibrary()
{
    return strdup(get_string_from_library().c_str());
}

Теперь в Swift я могу просто использовать это по принципу «выстрелил и забыл»:

print("String from C++ is: " + getStringAndFree(getStringFromLibrary()))
person Alex Cohn    schedule 13.12.2020