RustでNotesにアクセスしてみました
まずは、普通にCargoプロジェクトを作る。
$ cargo new mynotes
Created binary (application) `mynotes` package
このプログラムでは、Notes C APIを初期化し、メッセージを表示してすぐ終了する、非常に簡単なものになる。
動かすためには、Notesクライアントへのパスが必須になる。VSCodeの場合、タスクを作れば簡単に動作させることができる。.vscode/tasks.jsonファイルに、以下のように設定する。
// mynotes/.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Run mynotes (on Client;x86)",
"type": "shell",
"command": "cargo",
"args": ["run"],
"group": { "kind": "build", "isDefault": true },
"options": {
"env": {
"Path": "C:/Program Files (x86)/HCL/Notes;${env:Path}"
},
},
},
]
}
注意: $記号が全角になっていますが、これはnoteで保存に失敗するため、やむなく置き換えました。実際に使う時は半角$記号を使ってください。
次はsrc/main.rsファイルを編集して、次のようにする。
// mynotes/src/main.rs
// LotusNotes Rustライブラリを使用する。
use lnotes as ln;
fn main() {
// コマンドライン引数を取得する。
let args = std::env::args();
// Notes C APIを初期化する(NotesInitExtendedを呼び出す)。
ln::start(args, || {
// 初期化に成功したら実行するコード
println!("Hello, my notes!");
}, |status: u16| {
// 初期化に失敗したら実行するコード
println!("Initialize error!: status={}", status);
});
}
ここまでできたら、先ほど設定したタスク「Run mynotes (on Client;x86)」を実行してみる。
cargo run
Compiling mynotes v0.1.0 (C:\XXX\mynotes)
error[E0432]: unresolved import `lnotes`
--> src\main.rs:2:5
|
2 | use lnotes as ln;
| ^^^^^^^^^^^^ no external crate `lnotes`
For more information about this error, try `rustc --explain E0432`.
error: could not compile `mynotes` due to previous error
早速エラーで引っかかる。まだ存在しないモジュール lnotes を利用しているからだ。
use lnotes as ln;
useキーワードを使うと、「モジュールへのパスをスコープに持ち込む」ことができる。asキーワードは、持ち込んだパスにエイリアスを与える。なので、この文は「lnotesというモジュールを持ち込んで、lnというエイリアスを与える」ということになる。
現時点で lnotes というモジュールは存在しない。これから作成する「lnotes」モジュールは、notes.dll/notes.libをRustから使えるようにすることが目的だ。
Rustでは、notes.dllのような外部のライブラリを利用するには、「外部関数インターフェース(FFI: foreign function interface)」を利用する。2023年3月20日時点で、crates.io(Rust言語のライブラリが登録されている公開リポジトリ)にNotes C API用のライブラリは見当たらないので、自作することにした。
$ cd ..
$ cargo new --lib lnotes
--lib というオプションを付けると、そのクレートはライブラリベースで作成される。このオプションを付けない場合、--bin というオプションが指定されたのと同義になり、実行ファイルベースになる。
なお、実行ファイルかライブラリかは、ファイル名で見分けが付く。src/main.rsがあれば実行ファイル、src/lib.rsがあればライブラリとなる。
ちなみに、ライブラリテンプレートからできあがったsrc/lib.rsの初期状態は、以下のようになる。
// lnotes/src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
作成されたsrc/lib.rsでは、addというエキスポート関数が定義されていて、さらにテストコードも含まれている。定義しているソースコードのそばに、テストコードを記述できるというのは、「こんな関数を定義していて、このテストをパスすれば、正しく動作している」ということがわかるので、なかなか合理的だなと思う。
mynotes は、Notesクライアントベースのアプリケーションなので、32bitが前提になる。そのため、ターゲットとして i686-pc-windows-msvc 固定になるので、mynotes/.cargo/config.toml ファイルを作成して、次のような設定を追加しておく。
// mynotes/.cargo/config.toml
[build]
target = "i686-pc-windows-msvc"
さて、先のmynotesクレート側でライブラリlnotesクレートを使いたい場合、mynotes/Cargo.tomlの[dependencies]に以下の記述を追加する。
// mynotes/Cargo.toml
...
[dependencies]
lnotes = { path = "../lnotes" }
これで、さきほどの「use lnotes as ln」の箇所はパスできるが、新しい問題が発生する。
$ cargo run
Compiling lnotes v0.1.0 (C:\XXX\lnotes)
Compiling mynotes v0.1.0 (C:\XXX\mynotes)
error[E0425]: cannot find function `start` in crate `ln`
--> src\main.rs:9:7
|
9 | ln::start(args, || {
| ^^^^^ not found in `ln`
For more information about this error, try `rustc --explain E0425`.
error: could not compile `mynotes` due to previous error
ln(lnotes) には start なんていう関数は提供されていないというエラーである。lnotesライブラリは、テンプレートで作っただけで何もしていないので、当然の結果だ。なので、次のミッションは「lnotes::start」という関数を提供することだ。
lnotes クレート側では、32bit/64bitの両方でコンパイルする必要がある。そのため、 .cargo/config.toml でターゲットを固定する方法ではなく、ビルド時にターゲットを個別指定する方法を取る。
VSCodeであれば、 .vscode/tasks.json に次のようにする。
// lnotes/.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Build all",
"type": "shell",
"command": "echo Build all!",
"group": { "kind": "build", "isDefault": true },
"dependsOrder": "sequence",
"dependsOn": [
"Build lib (for Client;x86)",
"Build lib (for Server;x64)",
]
},
{
"label": "Build lib (for Client;x86)",
"type": "shell",
"command": "cargo",
"args": [
"build",
"--target", "i686-pc-windows-msvc",
],
"group": { "kind": "build", "isDefault": false },
},
{
"label": "Build lib (for Server;x64)",
"type": "shell",
"command": "cargo",
"args": [
"build",
"--target", "x86_64-pc-windows-msvc",
],
"group": { "kind": "build", "isDefault": false },
},
]
}
さて、 lnotes::start 関数の提供作業に入る。lnotes::start 関数は、コマンドライン引数と、正常時実行関数、失敗時実行関数の3つを引数に取る。
// lnotes/src/lib.rs
use libnotes_sys as sys;
use std::ffi::{CString, c_char, c_int};
/// NotesInitExtendedで初期化し、成功した時next関数を実行する。
/// 失敗した時はerror関数を実行する。
/// * args: 引数オブジェクト
/// * next: 成功時実行関数
/// * error: 失敗時実行関数
pub fn start<F, E>(
env_args: std::env::Args,
next: F,
error: E
) where F: FnOnce(), E: FnOnce(sys::STATUS) {
let c_args = env_args
.map(|arg| CString::new(arg).unwrap())
.collect::<Vec<CString>>();
let args = c_args
.iter()
.map(|arg| arg.as_ptr())
.collect::<Vec<*const c_char>>();
unsafe {
let status = sys::NotesInitExtended(
args.len() as c_int,
args.as_ptr()
);
if status == sys::NOERROR {
next();
sys::NotesTerm();
} else {
error(status);
}
}
}
Notes C API の初期化関数、NotesInitExtended を実行して、正常値が返ってきたら next 関数を、異常値が返ってきたら error 関数を実行する。
このコードの冒頭に、またしても未定義のモジュールが出現する。lnotes_sys モジュールは、lnotes 内モジュールとして定義する、いわば Notes C API のヘッダーファイルとしての役割を持たせる。Notes C API で提供されているヘッダーファイルはC言語用のみで、Rust用はもちろんなく、自作するしかない。DLLで提供されているエクスポート関数などを、Rustで再現するように定義する。
lnotes ディレクトリ内で、次のようにコマンドを叩く。
$ cargo new --lib libnotes-sys
Created library `libnotes-sys` package
クレートとしては libnotes-sys としたが、実は `-` はモジュール名としては使えない。なので、 libnotes-sys のクレート名を、lnotes/libnotes-sys/Cargo.toml で name の箇所を次のように変更する。
// lnotes/libnotes-sys/Cargo.toml
[package]
name = "libnotes_sys"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
次に、lnotes/Cargo.toml の方で、依存先に libnotes-sys(libnotes_sys) を追加する。ハイフンとアンダーバーの違いに注意する。
// lnotes/Cargo.toml
...
[dependencies]
libnotes_sys = { path = "libnotes-sys" }
libnotes_sysクレートで定義するNotes C APIのRust用ヘッダーは、膨大なものが想定されるので、早い段階からファイルを分割して提供する。最初の内は、Notes C APIのCヘッダーファイルのような構成での分割を試みる。
まずは global.h のような定義ファイルを最小の構成で。
// lnotes/libnotes-sys/src/global.rs
use std::ffi::{c_ushort, c_int, c_char};
pub type WORD = c_ushort;
pub type STATUS = WORD;
extern "system" {
pub fn NotesInitExtended(argc: c_int, argv: *const *const c_char) -> STATUS;
pub fn NotesTerm();
}
「extern "C"」と書く方法がよく紹介されているが、ここではうまく動かない。Notes C APIでは「extern "system"」としてようやく動作した。
次は globerr.h のような定義ファイル。
// lnotes/libnotes-sys/src/globerr.rs
use crate::global::*;
pub const NOERROR: STATUS = 0;
上記2つのファイルを提供するエントリポイントとして。
// lnotes/libnotes-sys/src/lib.rs
mod global;
mod globerr;
pub use crate::global::*;
pub use crate::globerr::*;
こうすることで、利用する側では「libnotes_sys::global::STATUS」とせずに「libnotes_sys::STATUS」と global の箇所を省略できる。
最後に2つほどおまじないを。1つは、ビルド用のコードでリンク先となるnotes.libファイルを、cargo/rustcに教えてあげるためのコード。「X:¥notesapi」の部分がNotes C API ライブラリの展開先になる。
// lnotes/libnotes-sys/build.rs
#[cfg(target_arch = "x86_64")]
fn main() {
println!(r"cargo:rustc-link-search=native=X:\notesapi\lib\mswin64");
println!(r"cargo:rustc-link-lib=dylib=notes");
}
#[cfg(target_arch = "x86")]
fn main() {
println!(r"cargo:rustc-link-search=native=X:\notesapi\lib\mswin32");
println!(r"cargo:rustc-link-lib=dylib=notes");
}
main関数が2つあるが、ターゲットのアーキテクチャが64bitか32bitかで有効な関数が変わる。C言語の #ifdef - #else - #endif マクロのようなものだ。
もう1つのおまじないがビルドタスクで、ここではVSCode用のタスクとして紹介する。
// lnotes/.vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Build all",
"type": "shell",
"command": "echo Build all!",
"group": { "kind": "build", "isDefault": true },
"dependsOrder": "sequence",
"dependsOn": [
"Build lib (for Client;x86)",
"Build lib (for Server;x64)",
]
},
{
"label": "Build lib (for Client;x86)",
"type": "shell",
"command": "cargo",
"args": [
"build",
"--target", "i686-pc-windows-msvc",
],
"group": { "kind": "build", "isDefault": false },
},
{
"label": "Build lib (for Server;x64)",
"type": "shell",
"command": "cargo",
"args": [
"build",
"--target", "x86_64-pc-windows-msvc",
],
"group": { "kind": "build", "isDefault": false },
},
]
}
「Build lib (for Client;x86)」が32bit用のビルド、「Build lib (for Server;x64)」が64bit用のビルドになり、「Build all」を実行すれば、2つ連続でビルドできる。「--target」オプションでターゲットを変えているのがミソになる。これにより、さきほどの build.rs で適用される main 関数が変わるわけだ。
ここで、libnotes-sysを含めたlnotesライブラリクレート全体のビルドが通った。あとは、mynotes実行クレートのビルドを通し、実行するだけだ。
mynotes 側でも、notes.lib(notes.dll)へのリンク先を聞かれるので、こちらでも build.rs ファイルを定義する。こちらは32bit限定だ。
// mynotes/build.rs
fn main() {
println!(r"cargo:rustc-link-search=native=X:\notesapi\lib\mswin32");
println!(r"cargo:rustc-link-lib=dylib=notes");
}
VSCodeでタスク「Run mynotes (on Client;x86)」を実行してみる。
フォルダー mynotes で実行するタスク: cargo run
Compiling mynotes v0.1.0 (C:\XXX\mynotes)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target\i686-pc-windows-msvc\debug\mynotes.exe`
Hello, my notes!
まとめ
わかってしまえば、RustでDLLにアクセスする方法は案外難しくない。しかし、基礎知識に乏しいままだと、Windows 32bit/64bitでのマルチターゲットを前提にしたビルド方法や、ライブラリクレートの構成など、どこから調べればいいか、手がかりに事欠く。ちなみに、これらの作業で気付いた、おもしろかった点はビルドにもrustコード(build.rs)が使えるところだ。