问题
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