見出し画像

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)が使えるところだ。

この記事が気に入ったらサポートをしてみませんか?