d.sunnyone.org
sunnyone.org

ページ

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終端あり/なしの変換にちょっと小細工がいる、という感じ。

コードはこちら。
// 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);
}
}
view raw hello-u8.rs hosted with ❤ by GitHub

「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を参照。
[package]
name = "rustdlltest"
version = "0.1.0"
authors = ["user <user@example.com>"]
# See http://doc.crates.io/manifest.html
[lib]
name = "foo"
path = "src/lib.rs"
crate-type = ["dylib"]
view raw Cargo.toml hosted with ❤ by GitHub

lib.rsに、no_mangleとextern "C"をつけて、関数を記述する。
#[no_mangle]
pub extern "C" fn hello(count: i32) -> bool {
if count < 0 {
println!("A negative count is not supported.");
return false;
}
for i in 0..count {
println!("Hello, World: {}", i);
}
return true;
}
view raw lib.rs hosted with ❤ by GitHub

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

あとは、
cargo build

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

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


呼んでみる

楽なのでC#から呼んでみるには、こんな感じ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace RustLibSample {
static class NativeMethods {
[DllImport("foo.dll", EntryPoint = "hello")]
public static extern bool Hello(int i);
}
class Program {
static void Main(string[] args) {
var r1 = NativeMethods.Hello(-1);
System.Console.WriteLine("Result for -1: {0}", r1);
var r2 = NativeMethods.Hello(3);
System.Console.WriteLine("Result for 3: {0}", r2);
}
}
}
view raw Program.cs hosted with ❤ by GitHub


結果はこう:
>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らしい。