将std::string转换为Swift String

3

简短版:

如何将从使用桥接调用的 .cpp 函数返回的 std::string 对象转换为 Swift 的 String

详细版:

我有一个用 C++ 编写的库,我必须使用 Swift 调用一些代码。我创建了一个桥接,在我的 Xcode 项目中添加了两个文件:

一个桥接头文件,允许 Swift 调用 C 函数(据我所知,Swift 不能直接调用 C++ 函数,因此需要通过 C 函数传递)

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

还有一个 .cpp 文件,它可以调用 C++ 函数,并且可以使用 extern "C" 定义 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)

在函数 getStringFromLibrary() 内设置断点来检查变量 s 的值,可以查看字符串的内容,这证明库函数已正确调用。
但是,.swift 文件打印了一些奇怪的符号而不是原始字符串。 将 getStringFromLibrary() 更改为以下内容。
extern "C" const char * getStringFromLibrary()
{        
    return get_string_from_library().c_str();
}

由于 Swift 代码打印真实字符串的前缀,我认为这是一个内存问题:很可能当 getStringFromLibrary() 结束时,get_string_from_library() 返回的 std::string 对象被销毁,因此通过 .c_str() 返回的指针所指向的内存不再可靠,这就是我从 Swift.print() 得到错误输出的原因。

从 .swift 文件中访问 std::string 并释放其内存的正确方法是什么?


1
你的 getStringFromLibrary 函数将会持有一个悬空指针,因为 s 是指向 get_string_from_library 函数分配并销毁的 const char*。我不熟悉 Swift,但你需要在堆上分配那个 const char* 并将所有权交给调用者。 - Cory Kramer
也许一个解决办法是在getStringFromLibrary()内调用malloc来为一个char*变量分配内存,将变量的内容复制到Swift字符串中,然后显式地调用free() - Nisba
@Nisba 是的,那是通常的做法。你可以考虑添加一个 freeStringFromLibrary 函数,而不是直接从 Swift 调用 free,这样就可以隐藏内存分配的底层方法。这将让你在内部使用 new[]malloc - Miles Budnek
4个回答

5
你可以编写Objective-C++包装器来处理C++代码。
桥接头文件:
#include "Wrapper.hpp"

Wrapper.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 */

Wrapper.mm:

#include "Wrapper.hpp"

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

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

Swift代码:

print(getStringFromLibrary())

[NSString stringWithUTF8String:] 方法将缓冲区的内容复制到一些内部存储中,ARC 管理释放它。你不需要定义 free_something()


4

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 对象的不透明 "句柄",让 Swift 访问其底层缓冲区,并由不同的函数释放它:

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中,你可以这样做:

现在在Swift中,您可以执行以下操作:

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

这种方法消除了额外的复制,但仍然不是异常安全的,因为调用者必须处理资源管理。

可能有一种方法将Objective-C++混合到解决方案中,以避免资源管理问题,但不幸的是我对Swift或Objective-C++不够熟悉,无法告诉你这个解决方案是什么样子。


std::string* s = new std::string(get_string_from_library()); 中,额外的副本不是只隐藏起来了吗? - Nisba
不,由 get_string_from_library 返回的字符串将被移动到新的 string 对象中。这只需要指针复制而不是复制整个字符串。 - Miles Budnek
1
用第一种方法来追求风格的点数:使用 strdup()free() 代替 new/copy/delete[] - Alex Cohn

2

我定义了一个函数(如果你喜欢,它也可以是静态的String扩展):

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

在C++中,

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

现在在Swift中,我可以简单地以一种快速的方式使用这个:

print("String from C++ is: " + getStringAndFree(getStringFromLibrary()))

1

我解决了,我发布了我最终使用的解决方案。

将来我可能会编写一个类,以便以更安全和自动化的方式管理删除操作。我会更新答案。

桥接头文件:

//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());
}

Swift代码:

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

Swift.print(myString)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接