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_rawfrom_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章节,提供了一个资源保持存活的例子。