ページ

2015-06-13

RustとWindowsのワイド文字列(LPCWSTR / LPWSTR)

最近?のWindowsでC FFIで文字列を受渡しをするとなると、ほぼ確実にでてくるのがワイド文字列(「Unicode」文字列)。それをRustでどう取り扱うかについて。

最初に感想を書いておくと、今のところ、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::OsStrExtstd::os::windows::ffi::OsStringExt (ソースを見るに1.0 stableなのにdoc.rust-lang.orgにない...)

変換関数

と、いうことで、便利なCStr/CStringは使えないものの、OsStr/OsStringを経由させればOK。ただし、NULL終端あり/なしの変換にちょっと小細工がいる、という感じ。

コードはこちら。

「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()のほうがいいかもしれない。取り扱っているコード次第だと思う。

2015-06-12

C++のラムダ式を利用してお手軽にOutputDebugString()を呼び出す

Windowsでデバッグ出力をしたいと思って、OutputDebugString()を呼ぼうとすると、printf的なフォーマットがないので、ちょっと文字列に数値なんかをつけたいと思うと面倒。sprintfを呼んでしまうというのもひとつだけども、バッファサイズをどうしようとか考えたくない。boost::formatがあればまぁそれをさらっと使えばいいと思うけども、今回のは「boost持ってくるほどのものでもなぁ」というサイズのコードのとき。

で、stringstream使えばいいかなということでベタで呼び出すとこうなる(OutputDebugStringWのほうがいいと思うけど、そこは適宜読み替えで)。
std::ostringstream str;
str << "Num: " << i << std::endl;
OutputDebugStringA(str.str().c_str());

sprintf呼ぶよりはいいと思うのだけど、何回も呼ぼうとするとstr変数がすでにあったりなかったりでややこしい。それで今回は、ラムダ式を使う。

このようなユーティリティ関数を用意して:
void outputDebug(std::function<void(std::ostringstream&)> func) {
 std::ostringstream str;
 func(str);
 OutputDebugStringA(str.str().c_str());
}

こう呼び出す:
outputDebug([&](std::ostringstream& s) { 
 s << "Num: " << i << std::endl;
});

どうだろう?ostringstreamの型名を書くのがちょっと悲しいけど、最初よりはいいんじゃないかなと思う。

C++らしさというのは全然わからないので、仰々しくならずにできる、もっといいやりかたがあればぜひ教えてほしい。

2015-06-07

RustでDLLを作る

期待通りの方法で作れた。ちなみに:
c:\>rustc --version
rustc 1.2.0-nightly (613e57b44 2015-06-01) (built 2015-06-02)
今回のは特にfeature使ってないから1.0でいけるかな?

DLLの作り方

まずプロジェクトを作る。
cargo new rustdlltest

Cargo.tomlを書き換えて、[lib]を作る。詳しくはhttp://doc.crates.io/manifest.htmlを参照。

lib.rsに、no_mangleとextern "C"をつけて、関数を記述する。

詳しくは、Rust bookの「Foreign Function Interface」を参照。

あとは、
cargo build

cargo build --release
で、target\debugあるいはtarget\releaseにfoo.dllができている。

Dependency Walkerで見てみると、いろいろな関数がexportされているが、その中で指定したhelloがexportされていることがわかる。


呼んでみる

楽なのでC#から呼んでみるには、こんな感じ。


結果はこう:
>RustLibSample.exe
A negative count is not supported.
Result for -1: False
Hello, World: 0
Hello, World: 1
Hello, World: 2
Result for 3: True

問題ない。boolってなによと思ってintにしてみたら、0と1らしい。