Rust 函数:返回 已分配字符串
与返回一个对象同样的原因,通过 FFI 返回一个分配的字符串很复杂: Rust 分配器 可以与 FFI 边界另一侧(语言)的分配器不同。它与传递一个字符串参数相同,也有处理 NUL 终止字符串的限制。
extern crate libc;
use libc::{c_char, uint8_t};
use std::ffi::CString;
use std::iter;
#[no_mangle]
pub extern fn theme_song_generate(length: uint8_t) -> *mut c_char {
let mut song = String::from("💣 ");
song.extend(iter::repeat("na ").take(length as usize));
song.push_str("Batman! 💣");
let c_str_song = CString::new(song).unwrap();
c_str_song.into_raw()
}
#[no_mangle]
pub extern fn theme_song_free(s: *mut c_char) {
unsafe {
if s.is_null() { return }
CString::from_raw(s)
};
}
这里我们使用一对方法into_raw
和from_raw
。先会将一个CString
转换成一个原始指针,这样也许可以跨过 FFI 边界。字符串的所有权,传递给了调用者,但调用者必须将字符串返回给 Rust,才能正确释放内存。
C
#include <stdio.h>
#include <stdint.h>
extern char *
theme_song_generate(uint8_t length);
extern void
theme_song_free(char *);
int main(void) {
char *song = theme_song_generate(5);
printf("%s\n", song);
theme_song_free(song);
}
在 C 版本,这没什么好玩的: char *
是返回的,可以打印,然后传回去,释放.
Ruby
require 'ffi'
class ThemeSong < FFI::AutoPointer
def self.release(ptr)
Binding.free(ptr)
end
def to_s
@str ||= self.read_string.force_encoding('UTF-8')
end
module Binding
extend FFI::Library
ffi_lib 'string_return'
attach_function :generate, :theme_song_generate,
[:uint8], ThemeSong
attach_function :free, :theme_song_free,
[ThemeSong], :void
end
end
puts ThemeSong::Binding.generate(5)
因为 字符串已分配,所以我们需要在超出范围时,确保释放分配。 像一个对象,我们的FFI::AutoPointer
子类,会为我们自动释放指针。
我们定义to_s
函数,使用 UTF-8 编码,将原始字符串延迟转换为一个 Ruby 字符串,并记住结果。Rust 生成的任何字符串,都是有效的 UTF-8。
Python
#!/usr/bin/env python3
import sys, ctypes
from ctypes import c_void_p, c_uint8
prefix = {'win32': ''}.get(sys.platform, 'lib')
extension = {'darwin': '.dylib', 'win32': '.dll'}.get(sys.platform, '.so')
lib = ctypes.cdll.LoadLibrary(prefix + "string_return" + extension)
lib.theme_song_generate.argtypes = (c_uint8, )
lib.theme_song_generate.restype = c_void_p
lib.theme_song_free.argtypes = (c_void_p, )
def themeSongGenerate(count):
ptr = lib.theme_song_generate(count)
try:
return ctypes.cast(ptr, ctypes.c_char_p).value.decode('utf-8')
finally:
lib.theme_song_free(ptr)
print(themeSongGenerate(5))
我们必须使用c_void_p
代替c_char_p
,作为类型的返回值,因为原c_char_p
,将自动转换为 Python 字符串。这个字符串将被 Python 不正确地释放,而不是由 Rust 释放。
我们换成c_void_p
,不要c_char_p
,获取值,并将原始字节编码为 UTF-8 字符串。
Haskell
{-# LANGUAGE ForeignFunctionInterface #-}
import Data.Word (Word8)
import Foreign.Ptr (nullPtr)
import Foreign.C.String (CString(..), peekCString)
foreign import ccall unsafe "theme_song_generate"
theme_song_generate :: Word8 -> IO (CString)
foreign import ccall unsafe "theme_song_free"
theme_song_free :: CString -> IO ()
createThemeSong :: Word8 -> IO (Maybe (String))
createThemeSong len = do
ptr <- theme_song_generate len
if ptr /= nullPtr
then do
str <- peekCString ptr
theme_song_free ptr
return $ Just str
else
return Nothing
main :: IO ()
main = do
song <- createThemeSong 5
case song of
Nothing -> putStrLn "Unable to create theme song"
Just str -> putStrLn str
在调用 FFI 方法之后,我们检查字符串是否是NULL
。 如果没有,我们使用peekCString
将其转换为 Haskell 字符串,并让 Rust 字符串 free
。
Node.js
const ffi = require('ffi');
const lib = ffi.Library('libstring_return', {
theme_song_generate: ['char *', ['uint8']],
theme_song_free: ['void', ['char *']],
});
function themeSongGenerate(len) {
const songPtr = lib.theme_song_generate(len);
try {
return songPtr.readCString();
} finally {
lib.theme_song_free(songPtr);
}
}
console.log(themeSongGenerate(5));
该字符串作为char *
返回,我们可以在将它传回去之前,通过调用readCString
转换为 JavaScript 字符串。
C\
using System;
using System.Runtime.InteropServices;
using System.Text;
internal class Native
{
[DllImport("string_return")]
internal static extern ThemeSongHandle theme_song_generate(byte length);
[DllImport("string_return")]
internal static extern void theme_song_free(IntPtr song);
}
internal class ThemeSongHandle : SafeHandle
{
public ThemeSongHandle() : base(IntPtr.Zero, true) {}
public override bool IsInvalid
{
get { return false; }
}
public string AsString()
{
int len = 0;
while (Marshal.ReadByte(handle, len) != 0) { ++len; }
byte[] buffer = new byte[len];
Marshal.Copy(handle, buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
}
protected override bool ReleaseHandle()
{
Native.theme_song_free(handle);
return true;
}
}
public class ThemeSong : IDisposable
{
private ThemeSongHandle song;
private string songString;
public ThemeSong(byte length)
{
song = Native.theme_song_generate(length);
}
public override string ToString()
{
if (songString == null) {
songString = song.AsString();
}
return songString;
}
public void Dispose()
{
song.Dispose();
}
static public void Main()
{
var song = new ThemeSong(5);
Console.WriteLine("{0}", song);
}
}
我们遵循与 Object 示例相似的模式: Rust 字符串是包含在SafeHandle
子类,和一个包装类ThemeSong
确保 Handle 是正确Dispose
的。
不幸的是,没有简单的方法将,指针读作一个 UTF-8 字符串。 C#有 ANSI 字符串和”Unicode”字符串 (实际上是 UCS-2) 的情况,但没有 UTF-8。 我们需要自己写。
Julia
#!/usr/bin/env julia
using Libdl
libname = "string_return"
if !Sys.iswindows()
libname = "lib$(libname)"
end
lib = Libdl.dlopen(libname)
themesonggenerator_sym = Libdl.dlsym(lib, :theme_song_generate)
themesongfree_sym = Libdl.dlsym(lib, :theme_song_free)
function generatethemesong(n::Int)
s = ccall(
themesonggenerator_sym,
Cstring, (UInt8,),
n)
out = unsafe_string(s)
ccall(
themesongfree_sym,
Cvoid, (Cstring,),
s
)
out
end
song = generatethemesong(5)
println(song)
我们使用 Cstring
数据类型表示为一个 NUL-终止 字符。而不是用 Julia 空间握住已分配的字符,这个例子用unsafe_string
建立了字符串的一个副本,由 Julia 自身管理,并在之后传回这个 Rust 字符串。该objects章节,提供了一个资源保持存活的例子。