最初に感想を書いておくと、今のところ、Rustでのwchar_t *なNULL終端された「UTF-16」文字列の取り扱いは、char *なNULL終端されたUTF-8文字列に比べるとあまり親切な取扱いではない。しかし、そこまで大変でもなかった。たぶんUTF-8じゃないマルチバイト文字列を扱うよりラク。
前提知識
コードを読む前の前提の話について、箇条書きで書くとこんな感じ:- Rustのふつうの文字列は、ざっくり言うとimmutableなstrと、mutableなString。
- std::str/Stringは、UTF-8文字列の型。NULL終端ではない。
- std::ffi::CStr/CStringは、UTF-8なchar *(NULL終端あり)をRustの世界で扱えるようにするラッパーの型。コピーが起きにくいようになっている。
- std::ffi::OsStr/OsStringは、Windowsのワイド文字列も扱えるようにした文字列の型(NULL終端でない)
- unixとwindowsのそれぞれにOsStrExt / OsStringExt traitがあり、OsStr/OsStringの出入りに使える。
- Windowsは、std::os::windows::ffi::OsStrExt と std::os::windows::ffi::OsStringExt (ソースを見るに1.0 stableなのにdoc.rust-lang.orgにない...)
変換関数
と、いうことで、便利なCStr/CStringは使えないものの、OsStr/OsStringを経由させればOK。ただし、NULL終端あり/なしの変換にちょっと小細工がいる、という感じ。コードはこちら。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Add to Cargo.toml | |
// [dependencies] | |
// winapi = "*" | |
// user32-sys = "*" | |
extern crate winapi; | |
extern crate user32; | |
fn from_wide_ptr(ptr: *const u16) -> String { | |
use std::ffi::OsString; | |
use std::os::windows::ffi::OsStringExt; | |
unsafe { | |
assert!(!ptr.is_null()); | |
let len = (0..std::isize::MAX).position(|i| *ptr.offset(i) == 0).unwrap(); | |
let slice = std::slice::from_raw_parts(ptr, len); | |
OsString::from_wide(slice).to_string_lossy().into_owned() | |
} | |
} | |
fn to_wide_chars(s: &str) -> Vec<u16> { | |
use std::ffi::OsStr; | |
use std::os::windows::ffi::OsStrExt; | |
OsStr::new(s).encode_wide().chain(Some(0).into_iter()).collect::<Vec<_>>() | |
} | |
use std::fmt; | |
use std::ptr; | |
#[no_mangle] | |
pub extern "C" fn hello2(ptr: *const u16) { | |
let y = from_wide_ptr(ptr); | |
println!("X: {}", y); | |
unsafe { | |
user32::MessageBoxW(ptr::null_mut(), | |
to_wide_chars(&format!("こんにちは: {}", y)).as_ptr(), | |
to_wide_chars("世界").as_ptr(), | |
0); | |
} | |
} |
「from_wide_ptr」は、from_wide()が&[u16]を受け取るので、スライスを作っていて、スライスを作る大きさを\0までの長さで計算している。
「to_wide_chars」は、encode_wide()でIteratorを得て、NULLだけを返すIteratorをSome(0).into_iter()で作ってchain()でくっついたIteratorを生成し、collect()でVecにしている。
一応呼び出し側のサンプルはこちら→https://gist.github.com/sunnyone/1d9e081df7eada21911d
(コンパイル方法はRustでDLLを作るの記事を読んでね)
注意書き
trait作ってprimitiveにimplしたほうがいいかもしれない。そこは実際に自分が使うときにやるかも(真面目にOsStringExtに実装してpull reqしたら取り込んでもらえる?プロセスの堅さどうなんだろ...)「from_wide_ptr」は、効率が悪いと思いつつ利便性は一番これがいいかなと思ってto_string_lossy()をownedにした(コピーした)Stringを返しているけども、これがいつでも最適解だとは思っていない。OsStringからはlosslessにencode_wide()できると言っているので、ただ引き回すだけならOsStringそのままがいいかもしれないし、厳密性が大事なやつは不正なバイト列があったとき(たぶんペアになってないサロゲートペアとか)に変換に失敗するinto_string()のほうがいいかもしれない。取り扱っているコード次第だと思う。