convert an std::string to a Swift String

天大地大妈咪最大 提交于 2021-02-19 04:24:26

问题


Short version:

how can I convert an std::string (object returned by a .cpp function called from a .Swift file using bridging) to a Swift String?

Long version:

I have a library written in C++ and I have to call some code using Swift. I created a bridge, adding two files in my Xcode projects:

a bridging header, which allows Swift to call C functions (afaik Swift can't call C++ functions directly, so it needs to pass through C functions)

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

and a .cpp files which can call (of course) C++ functions inside and can define C functions with 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;
}

I can access the return value from a .swift file using

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

Placing a breakpoint to check the value of s inside the function getStringFromLibrary() I can see the content of the string, so the function from the library is correctly called.

Anyway the .swift file prints some strange symbols and not the original string. Changing getStringFromLibrary() to be the following

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

I have as a consequence that the .swift code prints a prefix of the real string. This makes me think of a problem of memory: probably when getStringFromLibrary() exits, the std::string object returned by get_string_from_library() is destroyed, and so the memory pointed by the pointer returned with .c_str() is not reliable anymore, reason why I get wrong outputs from Swift.print().

What is the correct way to access an std::string from a .swift file and then release its memory?


回答1:


You can write an Objective-C++ wrapper to work with C++ codes.

bridging header:

#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 code:

print(getStringFromLibrary())

[NSString stringWithUTF8String:] copies the contents of the buffer into some internal storage and ARC manages freeing it. You have no need to define free_something().




回答2:


The std::string object owns the buffer to which a pointer is returned via c_str. That means that getStringFromLibrary returns a pointer to nothing.

There are several ways you could avoid that:

1) Copy the contents of the buffer somewhere long-lived, and return a pointer to that. This usually means allocating some memory via new[] or 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;
}

}

This method is simple, but it does incur the cost of an extra copy. That may or may not be relevant depending on how big str is and how often getStringFromLibrary is called. It also requires the user to deal with resource management, so it's not particularly exception-safe.

2) Return a opaque "handle" to a std::string object and let swift access its underlying buffer and free it by different functions:

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

}

Now in swift you can do something like this:

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

This approach eliminates the extra copy, but it's still not exception-safe since the caller has to deal with resource management.

There's likely a way to mix Objective-C++ into the solution to avoid the issue with resource management, but sadly I'm not familiar enough with Swift or Objective-C++ to tell you what that solution looks like.




回答3:


I solved, I post the solution I ended up with.

In the future I will probably write a class which will allow me to manage the delete operation in an automatic an safer way. I will update the answer.

bridging header:

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

wrapper:

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 code:

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

Swift.print(myString)



回答4:


I define a function (it could be a static String extension, if you prefer):

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

and in C++,

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

Now in Swift, I can simply use this in a fire-and-forget way:

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


来源:https://stackoverflow.com/questions/49805921/convert-an-stdstring-to-a-swift-string

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!