rust
Win32API
NAS
共有フォルダ

RustからWin32APIを呼び出す方法

はじめに

RustのFFIでCの関数を呼び出す方法を調べたところ、
Win32APIをうまいこと呼び出せるっぽいということを知りました。
そこでサンプルとして、ネットワーク資源(NAS上のファイル)にアクセスするコードを書いてみました。

僕の周りでRustの知名度が低かったので布教活動として、
そして、いろいろ調べても英語の記事しかヒットせず辛かったので
個人的な備忘録としてまとめます。

Rustって何?

FirefoxのMozillaによって開発されているナウいプログラミング言語。
低レイヤーな開発も可能なC++の後継で、WebAssembly対応(まだ仮)しているのでWeb系の重い処理が今後実装されるのでは!?
・・・というのが僕の印象です。

ちなみに先日公開されたFirefox QuantumのCSSエンジンがRustに置き換えられたとか。
http://itpro.nikkeibp.co.jp/atcl/idg/14/481542/092900421/

開発環境など

OS: Windows10
Rust: ver1.21.0

とりあえずコードをべたっと

挙動としては以下の通り
①ネットワーク資源へ接続し、ローカルデバイス("Z:\")として割り当て
②フォルダを指定し("Z:/sample")、その直下の全ファイルを取得。
③取得したファイル情報のうち、ファイル名をコンソールに表示

まだ不慣れなのでRust的な書き方ではないのですがご了承の程を。

main.rs
extern crate winapi;
extern crate kernel32;
extern crate mpr;

use winapi::minwinbase::WIN32_FIND_DATAA;
use winapi::minwindef::*;
use winapi::shlobj::INVALID_HANDLE_VALUE;
use winapi::winerror::NO_ERROR;
use winapi::winnetwk::*;
use std::string::*;
use std::ffi::*;
use kernel32::*;
use mpr::*;

fn main() {
    //ローカルの割り当て、ネットワーク上の資源
    let local_name = CString::new("z:").unwrap();
    let lpremote_name = CString::new("<資源名 or IPアドレス>").unwrap();
    //ファイル取得を行うディレクトリ
    let dir_path = String::from("Z:/sample");

    //接続に成功すれば、ファイル情報取得を実行
    if connect_resource(&local_name, &lpremote_name){
        get_fileinfo(dir_path);
    }
}


//ネットワーク接続
fn connect_resource(local: &CString, lpremote: &CString) -> bool {
    //return用
    let mut flg = true;
    //ユーザー名、パスワード
    let usr_name = CString::new("<ユーザー名>").unwrap().into_raw();
    let pass = CString::new("<パスワード>").unwrap().into_raw();

    unsafe{
        //接続を初期化(切断)
        WNetCancelConnection2A(local.as_ptr(), 0, FALSE);

        //引数のstruct
        let mut res = NETRESOURCEA{
            dwScope: 0,
            dwType: RESOURCETYPE_ANY,
            dwDisplayType: 0,
            dwUsage: 0,
            lpLocalName: local.to_owned().into_raw(),
            lpRemoteName: lpremote.to_owned().into_raw(),
            lpComment: CString::new("").unwrap().into_raw(),
            lpProvider: CString::new("").unwrap().into_raw()
        };
        //ネットワーク上の資源に接続
        let dw_res = WNetAddConnection2A(&mut res, pass, usr_name, CONNECT_UPDATE_PROFILE);
        if dw_res != NO_ERROR {
            flg = false;
        }
    }
    return flg
}


//ファイル情報取得
fn get_fileinfo(path: String) {    
    //パスをファイル検索用に加工
    let mut path_wk = path;
    path_wk.push_str("/*");
    //引数のstruct
    let mut file_info = WIN32_FIND_DATAA{
        dwFileAttributes: 0,
        ftCreationTime: FILETIME{dwLowDateTime:0, dwHighDateTime:0},
        ftLastAccessTime: FILETIME{dwLowDateTime:0, dwHighDateTime:0},
        ftLastWriteTime: FILETIME{dwLowDateTime:0, dwHighDateTime:0},
        nFileSizeHigh: 0,
        nFileSizeLow: 0,
        dwReserved0: 0,
        dwReserved1: 0,
        cFileName: [0; MAX_PATH],
        cAlternateFileName: [0; 14]
        };

    unsafe{
        //ファイル情報(1つ目)取得
        let target_path = CString::new(path_wk).unwrap();
        let dir_handle = FindFirstFileA(target_path.as_ptr(), &mut file_info);

        if dir_handle == INVALID_HANDLE_VALUE{
            panic!("フォルダが存在しません");
        }else{
            //フォルダ内のファイルをすべて見るまでループ
            loop {
                let file_name = CStr::from_ptr(&file_info.cFileName[0]).to_str().unwrap();
                println!("{}", file_name);

                if FindNextFileA(dir_handle, &mut file_info) == 0 {
                    break;
                }
            }
        }
    }
}

補足説明など

使用した外部クレート

Rustではライブラリのことを「クレート」と呼びます。
今回はWin32APIを使うために以下の外部クレートをソース内に宣言しました。

extern crate winapi;
extern crate kernel32;
extern crate mpr;

外部クレートを使う際には"extern"を置きます。なんとなくCっぽい感じです

「winapi」には、Win32APIのデータ型や各種リターンコードをRustで受け取るためのあれこれが用意されています。
そして「kernel32」「mpr」は各関数を呼ぶために必要です。
ちなみにこのクレート名、Win32APIの関数が格納されているlibファイル名 or DLL名と同じなので何が必要なのかは極めて明快。
クレートの詳細はについてこちらこちらをご参照ください。

また呼び出しているWin32API関数については下記参照
WNetCancelConnection2
WNetAddConnection2
FindFirstFile
FindNextFile

ビルドツールCargo

Rustプロジェクトの作成からビルドまで簡単にできるツール、それがCargoです(Rustを落としたらいっしょについてくる)。
プロジェクト作成時に「Cargo.toml」という設定ファイルを作ってくれて、
ここに必要な情報をごにょごにょ書いていきます。

Cargo.toml
[package]
name = "fileUtilRust"
version = "0.1.0"
authors = ["you <you@gmail.com>"]

[dependencies]
winapi = "0.2.4"
kernel32-sys = "0.2.2"
mpr-sys = "0.1.0"

[dependencies]には依存関係のあるクレート(ライブラリ)を記述しており、
初回コンパイル時にCargoが必要なリソースを持ってきてくれます。

ちなみにCargoでプロジェクトを作成すると、
デフォルトでgitのリポジトリになるのですごく便利です。

unsafe

実際に上記のクレート内のモジュールを使用している箇所を見てみると、
必ずunsafeブロックで囲まれていることが確認できます。

main.rsより抜粋
    unsafe{
        //接続を初期化(切断)
        WNetCancelConnection2A(local.as_ptr(), 0, FALSE);

        //引数のstruct
        let mut res = NETRESOURCEA{
            dwScope: 0,
            dwType: RESOURCETYPE_ANY,
            dwDisplayType: 0,
            dwUsage: 0,
            lpLocalName: local.to_owned().into_raw(),
            lpRemoteName: lpremote.to_owned().into_raw(),
            lpComment: CString::new("").unwrap().into_raw(),
            lpProvider: CString::new("").unwrap().into_raw()
        };
        //ネットワーク上の資源に接続
        let dw_res = WNetAddConnection2A(&mut res, pass, usr_name, CONNECT_UPDATE_PROFILE);
        if dw_res != NO_ERROR {
            flg = false;
        }
    }

Rustのコンパイラはプログラムの安全性を保障するためにかなり厳格なチェックを行います。
unsafeブロック内ではそのチェックを一時的に緩めてもらっています。
そのため、FFIでCの関数を呼び出すなど、Rust的に安全でない操作が可能になります。
(当然ながら安全でない操作なので、unsafeは必要最低限に抑えたいです)

structの初期化をスマートにしたかった

main.rsより抜粋
    let mut res = NETRESOURCEA{
        dwScope: 0,
        dwType: RESOURCETYPE_ANY,
        dwDisplayType: 0,
        dwUsage: 0,
        lpLocalName: local.to_owned().into_raw(),
        lpRemoteName: lpremote.to_owned().into_raw(),
        lpComment: CString::new("").unwrap().into_raw(),
        lpProvider: CString::new("").unwrap().into_raw()
    };

もともとC++でプロト版を作成した際、引数のstructは必要な要素のみ値を入れるだけでよかったのですが、
Rustは徹底してNULLを嫌う!
結局上記のようにすべて手打ちで初期化する以外方法が見つからなかったです。
標準ライブラリではNew()で初期化できるのですが、、、

Cの文字配列の取得方法

以下、ファイル名をコンソール出力している箇所です。

main.rsより抜粋
let file_name = CStr::from_ptr(&file_info.cFileName[0]).to_str().unwrap();
println!("{}", file_name);

file_nameには、Win32APIから返ったCの文字配列を「from_ptr()で受け取り」「to_str()でRustの文字列にキャストした」した値が入ってます。
その際、文字配列のインデックスは0で指定していますがちゃんと"文字列"が受け取れます。

なんでや?と思ってfrom_ptr()の中身を見ると疑問は解決しました。

c_str.rs
pub unsafe fn from_ptr<'a>(ptr: *const c_char) -> &'a CStr {
        let len = sys::strlen(ptr);
        let ptr = ptr as *const u8;
        CStr::from_bytes_with_nul_unchecked(slice::from_raw_parts(ptr, len as usize + 1))
    }

sys::strlen()でCの関数「strlen()」を呼び出しており、
引数の*ptrを起点にメモリを走査して値がNULLになるまでのbyte数(=配列の要素数)を返しています
で、配列の起点(ptr)と要素数(len)をもとに文字列を取得しているわけです。

なるほど、その手があったか!と感心しました。

参考資料

Win32APIを呼び出す方法について下記の記事を参考にしました。

Calling Win32 API with Rust FFI
Win32 GUI Programming In Rust Language

もしRustに興味を持たれた方は以下のリンクをぜひご参照ください!
https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/README.html