Rustプログラミング覚え書き

鷺澤伸介
(初稿 2023.9.20)
(最終改訂 2023.9.20)

「CDデータベース制作覚え書き」の追記にも書いたように、2023年8月にやっとこさRustをWindowsPCにインストールしました。公式が推奨するC++ Build toolsを使うバージョン(msvc版)はストレージを圧迫するためひとまず避けて、VSCodeでのビルドのためにDドライブに入れてあったMinGW-w64(わずか600MBちょい)を使うバージョン(gnu版)にしましたが、今回の試用においては特に問題は起こりませんでした。

RustでSQLite3を操作する

プログラミングの練習には、以前自分が作ったものを新しい言語で再現してみるのがよい、と聞いたことがありますので、「C++プログラミング覚え書き」のときに書いた「SQLを実行する簡易クラス」をRustで再現してみることにしました。予想どおり、その書法の難しさに苦労というか辟易する場面も少なくなかったとはいえ、設計図のようなものはすでにあるわけですから、それほど時間はかからずひとまず目標に到達することができました。再現作業が一段落してからしばらくは、「おお、ちゃんと動くじゃん何これ楽しいきゃっきゃっ!」という感じで、嬉々としていろんなSQLを発行して遊んでいたのですが、その高揚が落ち着いてからあらためてコードを眺めてみたら、気になるところが出てきました。何というか、見た目があんまりRustっぽくないのです。例えば、SQLファイルを読み込んでからそれを実行して表示するまでの関数をまとめた部分は、
pub fn sqlcontexec_txt(&mut self) {  // 構造体を受け取り、
    if !self.sqlfileread() {return;} // SQLファイルを読み込み、
    if !self.sqlvalid() {return;}    // 処理可能かどうかを調べ、
    if !self.sqlexec() {return;}     // バインド&全SQLを実行し、
    self.datadisplay();              // 返る結果があれば表示する。
}
となっています。それぞれの関数の実行が成功したかどうかがboolで返ってくるので、それがfalseならreturnで中断する、という処理にしたわけですが、これ、見た目はC言語みたいですよね。不成功時にはSQLite3が吐くエラーも関数内で拾って表示するので、C++のときと同等のものは作れたはずなのに、Rust的なエラー・ツールであるOption列挙体やResult列挙体を一切使っていないため、あまりRustの練習になっていないと感じられたのです。まあboolで足りているところをわざわざ面倒にする必要もないとは思うものの、OptionやResultの挙動を知るためにもここは無理してでも使っておいた方がよいという気がしましたので、その方針でやり直すことにしました。
★Rustインストール → SQLを実行する簡易クラスC++版のRustでの再現開始 → ひとまず終了 → その過程でC++版のコードにおかしな箇所があることに気づき、修正し、HPで報告 → Rust版の改造開始 → 終了 → 作業をもう一度頭からたどりながらこのページに記録、という流れです。

ラッパー系クレートを使わずに作ってみる

RustでSQLite3を使う方法を調べてみると、少なくとも2023年9月現在では「rusqliteというクレートを使うように」と説明されているのが普通です。このクレートはサードパーティ製ですが、どこを見ても公式製に準ずるような扱いとなっており、RustにおけるSQLite3操作ツールのデファクトスタンダードの地位を確立していることが分かります。しかし、今回はこの手のラッパー系クレートは使わず、なるべくSQLite3自体に直接触りながらやってみると最初から決めていました。C++版ではそのようにしたこと、SQLite3はC言語で書かれているのでそのコードが多少は読めること、すでに使いやすいようにラッパー化されているものをさらに自分用にカスタマイズするとなると、SQLite3との連絡が伝言ゲームになってしまって隔靴掻痒感が生じるであろうこと、などがその理由です。ただし、libcクレート(C言語とRustを連携させるためのもの)と-sysクレート(C言語ライブラリとRustを仲介するもの)だけは使わせてもらうことにしました。よって、Cargo.tomlには次のように記載しました。
[dependencies]
libc = "0.2.147"
sqlite3-sys = "0.15.2"
SQLite3用の-sysクレートはほかにもありますが、上記のものはプロジェクト専用のSQLite3をインストールしてくれるので、システム全体用の、パスを通したSQLite3がPCに入っていないのならば、これが重宝です。
★使用するクレートは少なくした方がビルド時間の短縮になるような気がしたので、最初は-sysも使わず、「組込みRust」の「9.1.Rustと少しのC」を参考に、必要なSQLite3の関数と定数のインターフェース定義だけを自分で書くことも考えました。しかし、「面倒であり間違いの元である手動のインタフェース生成」とも書かれていたため、ここはやはり既製品を使うのが無難と考え、自力で書くのは諦めました。
★クレートのバージョンは、すべてのバージョンが対象となる「= "*"」とすることもできますが、公式のエディション・ガイド(2018?)には「Crates.io disallows wildcard dependencies(Crates.ioはワイルドカードの依存関係を禁じます)」と書かれています。ということは、一般ユーザーもワイルドカードの使用はやめた方がよいということなのでしょうか?
★なでしこやPythonからSQLite3を使うのは簡単でしたが、やはり隔靴掻痒感はありました。C++によるSQLite3操作は面倒だったけれども、自分がしたいようにできたという印象です。

ソースファイルの配置設計

Rustでソースファイルが複数になった場合は、それらの配置に配慮する必要があります。これについては、何となく理解するだけでもけっこう時間がかかりました。
今回の制作というか練習では、一つの構造体とそのメソッド群にほぼすべての機能を持たせることになります。となると、一つの構造体を形成するコードであっても量が多く、それを一つのファイルにまとめてしまうと、長くなりすぎて扱いにくくなるでしょう。C++の方では一つのファイルに全コードを書き込んで、main関数以外で340行ほどになりました。自分的には1ファイル分としてはこれでもちょっと長いかなという印象で、Rust版がそれより短く書けるとはとうてい思えませんから、今回はファイルを分けることにしました。
複数のソースファイルをあたかも一つのファイルのように扱うためには、一つのディレクトリにそれらのファイルを入れておき、そのディレクトリと同じ名前のrsファイルで管理する、という方法があります。ディレクトリ名と管理ファイル名は必ず同じ名前でなければなりません。管理ファイルはディレクトリに貼られたラベルのようなもので、管理ファイルにコードを書き込むと、あたかも「ディレクトリの箱の表面」にコードを書き込んでいるのと同じような感じになります。
★ちなみに、ルート階層の管理ファイルはmain.rsであるようです。
src
├── main.rs
├── func.rs  // ←ここに構造体を定義すればfunc内から自由に使える。
└── func
     └── sqlexec.rs
     └── tools.rs
// main.rsからは自由に使えないので、そこから使う必要がある場合は、
// 構造体自体と必要なフィールドすべてをpubにしておく必要がある。
上の例では、func.rsで定義された構造体(名前をSqlとします)は、func自身やfunc配下のsqlexecやtoolsの間でなら自由に生成したりやりとりしたりできますが、mainからではできません。mainに「mod func;」とか「use func::Sql;」とか書き込んでもダメなのです。どうしてもmainでインスタンスを生成したりそのフィールドにアクセスしたりしたければ、funcにある構造体の定義にpubを付けるしかありません。とはいえ、スコープはなるべく狭くするというプログラミングの原則もありますし、できるだけパブリックにしないで済むような構造になるよう工夫するべきなのでしょう。
★funcの子であるsqlexecとtoolsは、funcに書いたSql構造体が使いたければ、「use super::Sql;」あるいは「use crate::func::Sql;」と書いておく必要があります(superは一つ上の階層を、crateはルート階層を指す)。親のfuncも、「mod sqlexec;」「mod tools;」と書かないと、子の所有物が使えません。
今回の練習で目標とするところは、C++でやったのとだいたい同じです(Rust版では少しだけ内容を変えました)。すなわち、同じコマンドで
などと引数を変えれば、それぞれがそれぞれに適したクエリ処理をする、という機能を実現することです。dbnameはデータベース名, sqlsはSQL文の連続体, valueはプレースホルダにバインドする値、sqltxtはそれらを箇条書きしたテキストファイルです。引数を一つも書かない場合は、デフォルトで設定したSQLファイルを対象とします。SQL文はいくつ連続していてもよく、プレースホルダもいくつあっても対処可能とし(ただし使える文字は「?」のみとし、ナンバリングには非対応)、後者は上記のように可変数引数とします。そうすると、必要なフィールドは次のようになるでしょう。
Rust:構造体の定義 [func.rs]
pub mod sqlexec;  // 子要素をこのファイルで使うために読み込む。
pub mod tools;    // mainからの使用を想定してpubを付けておくが、
                  // 付けないで済むなら付けない方がよさそう。
struct Sql {  // こちらはmainからは呼ばない想定でpubはなし。
    sqlfile: String,  // SQLファイル名
    dbname: String,   // データベース名
    sql: String,      // Sql文(いくつ連結させても可)
    values: Vec<String>,  // プレースホルダ値格納用ベクター
    resultdata: Vec<Vec<String>>,  // 結果格納用2次元ベクター
}  
あとは子の両ファイル(sqlexec.rsとtools.rs)に
use super::Sql;  // use crate::func::Sql; でも可。
と書き込んでおけば、pubを一つも付けることなく、func親子兄弟の間ではこの構造体を自由に扱うことができます。Sql構造体をmainから直接呼び出さねばならない機会は作らないつもりですし、pubはそれがどうしても必要になったときに付ければ十分でしょう。
★もしmainが、同僚funcの子供たち、sqlexecやtoolsにアクセスしたい場合は、まずその親であり管理者であるfuncにお伺いを立てる必要があります。つまり、func内の「mod sqlexec;」「mod tools;」をpubにしておく必要があるわけです。
コンストラクタっぽいものも作っておきましょう。
Rust:構造体インスタンスを作る関数 [func.rs]
impl Sql {
    pub fn new() -> Sql {
        Sql {
            sqlfile: String::from("sqlval.txt"),
            dbname: String::new(),
            sql: String::new(),
            values: Vec::new(),
            resultdata: Vec::new(),
        } 
    }
}
// 「new」という関数名は普通に使ってよいらしい。
// フィールドの初期値は全部空白でもよいが、
// SQL実行関数に引数を書かない場合も想定して、
// SQL文記載ファイルのみデフォルト値を設定した。

C++のコードをRust用に書き直す:その1

それでは、C++で書いたときのコードを少しずつこちらで再現していきます。まず、下記のコードをsqlexec.rsとmain.rsに書き入れて実行してみました。
Rust:データベースを開いてみるその1 [sqlexec.rs]
use std::ffi::{CString, CStr, c_int};
use std::ptr;

use sqlite3_sys::*;

use super::Sql;

impl Sql {
    pub fn sqlexec(&mut self) {
        let db_name = CString::new(&*(self.dbname))
                                .unwrap();  // ★
        let mut db = ptr::null_mut();
        let flags = SQLITE_OPEN_READWRITE as c_int; 
        let rc = unsafe {sqlite3_open_v2(
                db_name.as_ptr(), &mut db, flags, ptr::null()
                ) as i32
            };
        if rc == SQLITE_OK {
            println!("{}", "データベースを開きました!");
            let _ = unsafe {sqlite3_close_v2(db) as i32};
        } else {
            print!("{} のエラー:", "sqlite3_open_v2");
            let cstr = unsafe {sqlite3_errmsg(db)};
            let emsg = unsafe {CStr::from_ptr(cstr)
                .to_str()
                .unwrap()};  // ★
            println!("{}", emsg);
            println!("{}", "データベースを開けませんでした!");
        }
    }
}

pub fn opentest(){
    let mut q = Sql::new();
    q.dbname = "test.db".to_string();
    q.sqlexec();
}
// データベース名を空文字列にするとテンポラリファイルが作られ、
// ファイルがなくても処理が成功してしまうので注意。
★Rustでは、SQLite3の関数や定数を使うのにまずRust用のインターフェース定義を書かねばならないこと(上記のように、これは-sysクレートに頼りました)、C言語文字列とRust文字列双方向への変換がいちいち必要なこと、C言語由来の関数やポインタにはunsafeブロックを強要されることなど、面倒くさいったらありゃしません。ラッパー系クレートを介さずSQLite3を直接操作するのなら、SQLite3のコードをそのまま使えるC/C++の方が、どう考えても圧倒的に有利です。書き慣れないわ面倒くさいわですぐ嫌気が差しても不思議はなかったのに、数日のうちに「ま、Rustってのはこんなもんだよね」と諦められるというか受け入れられるようなったのは、VSCodeのrust-analyzer──公式によると「Rust言語のための言語サーバープロトコルの実装」だそうで、要するにコードの入力支援をしてくれる拡張機能──の力が大きかったと思います。上の「&*(self.dbname)」の箇所なんか、rust-analyzerが「こう書くんですよ~」と教えてくれたわけで、こんな書法、自分の頭ではそう簡単には思いつかなかったと思います。「参照外ししたものをまた参照するの?」と、いまだに少しもやもやしてはいるものの、まあそれでエラーが消えて動くようになったのですから、Rust的には正解なのでしょう。rust-analyzerは、慣れないうちはその膨大な補助表示がうるさく感じられたものですが、使っていくうちにいつの間にかそれに頼っている自分に気づきましたし(特に推論された型の具体表示)、Rust初心者には必須のツールと言ってもよいのではないでしょうか。……Rustの代表的なつまずきポイントである「所有権」には、あまり引っかからなかったように思います。当初は「困ったら変数をcloneすりゃいいでしょ」くらいに思っていたのですけれども、それは一度もやらずに済みました。C++の方で、変数は原則としてconstで使い(もちろんレガシー表記は捨ててEast const=右配置にするべきです)、その受け渡しには極力参照を使う、という習慣が付いていたのが幸いしたかもしれません。
Rust:データベースを開く関数の実行関数 [main.rs]
mod func;

fn main() {
    func::sqlexec::opentest();
}
実行結果
sqlite3_open_v2 のエラー:unable to open database file
データベースを開けませんでした!
大丈夫そうですね。まだデータベースファイルがない状態なので、このエラーが出るのは正常です。SQLite3が出すエラーも表示されましたし、残りのステップもこの感じで進めればいけそうな気がします。
★実行にはPowerShell7を使っています(たまにコマンドプロンプトも)。文字コードはShift-JISのままでも正しく表示されますが、念のためUTF-8に切り替えています。ただ、PowerShellの場合、「chcp 65001」と打ち込んだだけでは、画面表示は大丈夫でもテキストファイルに書き出すと文字化けしてしまいます。そこで、アドレスバーからの起動時に「pwsh -NoExit -Command "chcp 65001"」と打ち込んでみたところ、今度はテキストファイルも文字化けしませんでした(最初の「pwsh」を「powershell」とすると、古い方のPowerShellが起動します)。アドレスバーは履歴を記憶するので、二度目からは「p」だけ打って、あとはプルダウンから選ぶだけで済みます。また、テキストファイルに書き出すコマンドは、シンプルに「cargo run > t.txt」、画面にも表示するなら「cargo run | tee t.txt」でいけます。
★VSCodeは、MacTypeがインストールされていると「code .」コマンドによる起動が失敗するようです(私の環境だけ?)。MacTypeのプロセスマネージャーからVSCodeのプロセスを除外すれば起動できるようになりますが、自分としてはMacTypeによる美しいフォント表示は捨てられないので、VSCodeの起動はもっぱら右クリックメニューから行うようにしています。……MacTypeは、一度使い始めるとそれなしではいられなくなる依存性?中毒性?の高いアプリですから、導入するのなら「もう引き返せなくなるかも」という覚悟が必要です。まあそれも、いつまで経ってもフォントが汚いWindowsが悪いんですが。時々、本当にこの点だけのために、フォントが美しいMacやLinuxに乗り換えたくなります。
ただし、気になる点もあります。一つは、unwrap()を2回使ってしまっていること(★の箇所)。もう一つは、SQLite3のエラー処理コードは、これから同じような処理が何度か出てくるので、関数化した方がよいということです。
unwrap()を使う必要があるということは、返却値がOption列挙体かResult列挙体でラップされているということで、それはすなわち「CString::new()とto_str()は《失敗するかもしれない処理》である」ということを意味しています。よって、そのような箇所では、失敗の場合どうするかをプログラマが決める必要があります(panic終了でよいのならこのままでOK)。
SQLite3のエラー処理については、C++のときはそのまま毎回書いていましたが、どれもだいたい同じ内容なので、関数化した方がよさそうです。ただ、この処理の中にはto_str()が含まれているため、unwrap()で値を取り出すとそこでpanicという事態も考えられます。
Rustには、「正しく処理されたら正しい値を返して継続、エラーならそこで中断してエラー内容を呼び出し元に返すマクロ」として「?」があります。コマンドの最後に「?」をくっつけるだけで成否の分岐を書かなくても済むということですから、そんな便利なものを使わない手はありません。ただし、「?」には、その関数がOption型かResult型を返すものであること、呼び出し元もその関数と同じ型(Option型かResult型)を返すものであること、Result型の場合はエラーの型が呼び出し元と一致すること、などの条件があります。要するに、Option型を返す関数内ではOption型を返す関数で「?」を使うことができ、Result型を返す関数内ではResult型を返す関数で「?」を使うことができるけれども、後者の場合はエラーの型が一致している必要がある、となるでしょうか。
「?」マクロは、値をunwrapしてから返してくれます(その点でも便利ですよね)。そこで、上のコードで、CString::new()とto_str()の箇所からunwrap()を取って、代わりに「?」を付けてみると、コンパイラに「?はOptionかResultを返す関数の中でしか使えません」と言われます。それならと、呼び出し元のsqlexecをResultを返すようにしてみても、エラーの型が、CString::new()の方は「NulError」、to_str()の方は「Utf8Error」と異なっているため、どちらかに合わせるとどちらかがエラーになってしまいます。
こういうケースは極めて頻繁に発生するらしく、公式でも「Example」の「18.5.複数のエラー型」で対処法が説明されていますし、サードパーティ製クレートにもエラー型変換専用の有名なものがあるようですが、とりあえずいちばん手っ取り早いのは、Result<T, Box<dyn Error>>を利用する方法でしょう。何でも、Resultのエラーの方にBox<dyn std::error::Error>と書いておけば、エラーの型違いを気にしなくてもよくなるのだとか。お手軽な方法には当然欠点もあるのでしょうが、まだRustには触り始めたばかりで難しい処理を書くのは無理ですから、今はさしあたりこの方法を採用したいと思います。……書き直すついでに、SQLite3がエラーのとき、メッセージを表示してからclose_v2でデータベースを閉じる部分を関数化しておきます。
Rust:データベースを開いてみるその2 [sqlexec.rs]
use std::ffi::{CString, CStr, c_int};
use std::ptr;

use sqlite3_sys::*;

use super::Sql;

// エイリアスで簡潔に書けるようにする。公式の真似。
type Result<T> = 
    std::result::Result<T, Box<dyn std::error::Error>>;

impl Sql {
    pub fn sqlexec(&mut self) -> Result<()> {
        let db_name = CString::new(&*(self.dbname))?;
        let mut db = ptr::null_mut();
        let flags = SQLITE_OPEN_READWRITE as c_int;  
        let rc = unsafe {sqlite3_open_v2(
                db_name.as_ptr(), &mut db, flags, ptr::null()
                ) as i32
            };
        errmsg(rc, "sqlite3_open_v2", db)?;
        Ok(())
    }
}

// SQLite3のエラー処理ではほぼこの関数↓を使い回すことができそう。
// プリペアドステートメントのファイナライズ処理は入れず、
// それが必要な場合でもclose_v2のガベージ処理に解放を託す。
// to_str()のエラー以外、すなわちSQLite3エラーの場合は、
// データベースを閉じ、エラーメッセージをErrに入れて返す。
pub fn errmsg(rc: i32, s: &str, db: *mut sqlite3) -> Result<()> {
    if rc == SQLITE_OK {
        Ok(())
    } else {
        let em = unsafe {sqlite3_errmsg(db)};
        let ems = unsafe {CStr::from_ptr(em).to_str()?};
        let mut emf = format!("{} のエラー:{}", s, ems);
        let rc2 = unsafe {sqlite3_close_v2(db) as i32};
        if rc2 != SQLITE_OK {
            let s2 = "sqlite3_close_v2";
            let em2 = unsafe {sqlite3_errmsg(db)};
            let ems2 = unsafe {CStr::from_ptr(em2).to_str()?};
            let emf2 = format!("\n{} のエラー:{}", s2, ems2);
            emf += &emf2;
        }
        Err(emf.into())
    }
}

// main関数にはエラーを委譲せず、ここで結果を簡易表示させる。
// ここではもう?を使う必要がないので、返り値もなしとする。
// sqlexec関数が返すOkは空タプル()ゆえ、値を処理する必要はない。
pub fn opentest() {
    let mut q = Sql::new();
    q.dbname = "test.db".to_string();
    let res = q.sqlexec();
    match res {
        Ok(_) => println!("無事完了!"),
        Err(e) => println!("{}", e),
    }
}
★上のerrmsg関数で、エラーになったときにsqlite3_close_v2を呼んでクローズさせていますが、そのsqlite3_close_v2がエラーになることも当然考えられます。よって、「if rc2 != SQLITE_OK」のブロックで、その場合のエラーメッセージも追加しています。
実行結果
sqlite3_open_v2 のエラー:unable to open database file
エラー処理を関数化したたことや、「?」が使えるようになったことなどのおかげで、データベースオープン(①)の部分がずいぶん簡潔になりました。実行結果を見ても、先と同様ちゃんとSQLite3のエラーが拾えています。この後は、②プリペアドステートメント生成、③プレースホルダへのバインド処理、④SQL実行、⑤ファイナライズ、⑥データベースクローズと続き、いずれも①ができたのならその応用が利くはずですから、一気に作ってしまいましょう。②~⑤は複数のSQL文に対応するためにループ処理となります。
Rust:SQLを実行する関数と表示する関数 [sqlexec.rs]
use std::ffi::{CString, CStr, c_int, c_void};
use std::ptr;

use sqlite3_sys::*;

use super::Sql;

type Result = 
    std::result::Result<T, Box<dyn std::error::Error>>;

impl Sql {
    // ================================================================
    // 1.データベースオープン。
    pub fn sqlexec(&mut self) -> Result<()> {
        let db_name = CString::new(&*(self.dbname))?;
        let mut db = ptr::null_mut();
        let flags = SQLITE_OPEN_READWRITE as c_int;  
        let rc = unsafe {sqlite3_open_v2(
                db_name.as_ptr(), &mut db, flags, ptr::null()
                ) as i32
            };
        errmsg(rc, "sqlite3_open_v2", db)?;
        // ================================================================
        // SQL文をC文字列に変換。
        let csql = CString::new(&*(self.sql))?;
        let mut ptrsql = csql.as_ptr();
        // ループ開始。
        let mut vn = 0;      // 値コンテナ用カウンタ。
        let mut sqlcnt = 1;  // 何文目まで完了したかを確認するカウンタ。
        while unsafe {*ptrsql != 0} {  // ここの「0」はヌル文字('\0')。
            // ================================================================
            // 2.プリペアドステートメント生成。
            let mut stmt = ptr::null_mut();
            let rc = unsafe {sqlite3_prepare_v2
                (db, ptrsql, -1, &mut stmt, &mut ptrsql) as i32
            };  // 第5引数にSQL文字列のポインタを指定。
            errmsg(rc, "sqlite3_prepare_v2", db)?;
            // ================================================================
            // 現在処理中のSQL文の表示。確認用なので後で消す。
            let c_c_char = unsafe {sqlite3_sql(stmt)};
            let slice = unsafe {CStr::from_ptr(c_c_char)};
            println!("実行中のSQL文 = {}", slice.to_str()?);
            // ================================================================
            // 3.バインド処理。
            let mut bn = 1;
            let c_sql = unsafe {sqlite3_sql(stmt)};
            let currentsql = unsafe {CStr::from_ptr(c_sql)}
                                    .to_str()?;
            if !self.values.is_empty() {
                for c in currentsql.chars() {
                    if c == '?' {
                        let cstr = CString::new(&*(self.values[vn]))?;
                        let bindword = cstr.as_ptr();
                        vn += 1;
                        let rc = unsafe {sqlite3_bind_text(
                            stmt, bn, bindword, -1,
                            // ★★★SQLITE_TRANSIENT
                            Some(std::mem::transmute::
                                <isize, extern "C" fn(*mut c_void)>
                                (SQLITE_TRANSIENT as isize))
                        )} as i32;
                        errmsg(rc, "sqlite3_bind_text", db)?;
                        bn += 1;
                    }
                }
            }
            // ================================================================
            // 4.SQL文実行。ループは返ったデータをベクターに順に格納するため。
            loop {
                let rc = unsafe {sqlite3_step(stmt) as i32};
                if rc == SQLITE_ROW {
                    let col = unsafe {sqlite3_data_count(stmt) as i32};
                    let mut v_temp = Vec::new();
                    for n in 0..col {
                        let c_data = unsafe {
                            sqlite3_column_text(stmt, n) as *const i8};
                        let mut data = String::new();
                        if !c_data.is_null() {
                            let str_data = unsafe {CStr::from_ptr(c_data)};
                            let d = str_data.to_str()?;
                            data = d.to_string();
                        }
                        v_temp.push(data);
                    }
                    self.resultdata.push(v_temp);
                } else if rc == SQLITE_DONE {
                    break;
                } else {
                    println!("{} 文目でエラー発生!", sqlcnt);
                    errmsg(rc, "sqlite3_step", db)?;
                }
            } // loopブロックの終了。
            // ================================================================
            // 5.ファイナライズ。これでSQL文1文の処理完了。
            let rc = unsafe {sqlite3_finalize(stmt) as i32};
            errmsg(rc, "sqlite3_finalize", db)?;
            sqlcnt += 1;
        } // whileブロックの終了。
        // ================================================================
        // 6.データベースクローズ。あえて無印closeを使う。
        // 無印では未解放オブジェクトがある場合BUSYエラーとなるので、
        // ガベージのチェックに使える。
        // こちらのエラー後は普通にv2でクローズを試みる。
        let rc = unsafe {sqlite3_close(db) as i32};
        errmsg(rc, "sqlite3_close", db)?;
        Ok(())
    }

    // 結果が返った場合に表示する関数。
    pub fn datadisplay(&mut self) {
        if !self.resultdata.is_empty() {
            for datarow in self.resultdata.iter() {
                for i in 0..datarow.len() {
                    // データがないフィールドは書き出さない。
                    if datarow[i].is_empty() {continue;}
                    // 最初以外は区切りの半角スペースを頭に付ける。
                    if i != 0 {print!(" ");}
                    print!("{}", datarow[i]);
                }
                print!("\n");
            }
        } else {
            println!("返ったデータはありません。");
        }
    }
}
// この下↓に上で書いたerrmsg関数とopentest関数がある。
★C++は、「あまりにも神秘にして幽邃な森」(by池田亀鑑)のようで、なるべくなら敬遠したい気持ちが強いのに、やはりCに比べるといろいろ便利なものですから、どちらでもいい場合はどうしてもC++の方を選ぶことになります。C言語は、C++を書くようになってからはめったに書かなくなっており、そのため書法をすぐ忘れてしまいます。上のsqlite3_prepare_v2あたり(35行目前後)なんかも、今回この再現コードを書きながら、ちょっと混乱してしまいました。今、自分のために整理すると、
《オリジナルC言語コード》
sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail)
──第2、第5引数のconstはcharを修飾するので、文字列の変更は不可、ポインタの変更は可。
→ 《上のRust用コード》
sqlite3_prepare_v2(db, ptrsql, -1, &mut stmt, &mut ptrsql)
ここで、SQL文の変数を上のコードのように
・csql …… SQL文文字列(複数のSQL文の連続体、変更不可)
・ptrsql …… SQL文文字列csqlの先頭へのポインタ(変更可)
とした場合、
・文字列にアクセスしたいとき …… ptrsql(参照外し不要、'\0'まで連続でアクセスされる)
・文字列の1文字だけにアクセスしたいとき …… *ptrsql(2文字目以降はポインタ演算で)
・ptrsqlの中身(SQL文字列への先頭アドレス)を2文目の頭、3文目の頭と書き換えていきたいとき …… ptrsql = [new address]
・ptrsqlの中身(SQL文字列への先頭アドレス)を関数への参照渡しで書き換えたいとき …… &ptrsqlを関数へ → その関数内 *ptrsql = [new address]
となります。「すでに第2引数があるんだから、第2引数のアドレスでしかない第5引数っていらなくね?」という疑問も浮かびますが、第5引数が「NULL」の場合はSQL文の2文目以降の先頭ポインタは無視する仕様(=SQL文が複数連続していても最初の1文しか処理しない仕様)なので、そのスイッチとして必要なのでしょう。
ちなみに、Rustではポインタ演算はできないので、ポインタを前後させたいときはoffset()を使います。ヌル文字も「'\0'」とは書けないみたいなので、まあ「0」でいいんだろうなと思ったら、それで大丈夫でした。今試しに、SQL文を「SELECT * FROM test;」と19文字にして、
unsafe {println!("{}", *ptrsql.offset(19));}
と「20文字目」を取り出させてみたところ、
0
と表示されました。やはりヌル文字は「0」と書いて正解のようです。
これでコアの部分はC++版を再現できました。最初のデータベースオープンの部分でだいたいやり方は分かったので、その下はほぼ機械作業のようなものでした。ただし、一箇所だけすごく苦労した部分があります。sqlite3_bind_textの第5引数の書き方です(上の60行目あたりの★★★の下)。これは型がvoid(*)(void*)ですから、ボイドポインタを引数とする何も返さない関数を呼ぶということで、公式の説明によると、第3引数(SQL文の最大バイト長、普通に終端'\0'までを読めばよいのならば負数を指定)の値を破棄するデストラクタを呼ぶためのものだそうです。ただ、第5引数には実際には定数SQLITE_TRANSIENT=-1を指定することが多く、このプログラムのようにバインド値に一時的(トランジエント)な変数を使う場合はこれを指定します。そうすると、SQLite3は値を直ちにコピーして確保するとのことですから、元の値がなくなってしまっても処理が可能になるわけです。ほかに定数SQLITE_STATIC=0がありますが、こちらはSQLite3が値をコピーしない代わりに変数が不変(スタティック)である場合にしか使えず、実際本プログラムでこれを指定するとバインドに失敗します。そういうわけで、第5引数には「SQLITE_TRANSIENT」または「-1」と書きたいのですけれども、それをするとコンパイラに「型が違う」と拒否されてしまうのです。第5引数にはvoid(*)(void*)型、Rust的には「Some<extern "C" fn(*mut c_void)>型」が期待されている、と(sqlite3-sysを作った人がなぜこれをOption型のSomeにしているのかよく分かりませんが、何かしかるべき理由があるのでしょう)。C++では何も考えずに定数を書いておけばOKだったので困り果てましたが、ここでは要するに、ポインタ型を-1に「見せかけて」渡すことができればよいのは?と考えました。で、上のコードのように、型を再解釈する std::mem::transmute の出番となったわけですが、これを見つけ出すまでにいったい何時間費やしたことか……。だいたい、初心者が使うような関数ではないですよね、これ。再解釈する方とされる方は同じ大きさでなければダメとのことで、最初は「64ビットPCだし、i64かな~」などとやっていたのですが、isizeというまさに「ポインタサイズ整数型」があったことを思い出し、「あ、これなら環境に合わせて大きさを変えてくれるじゃん」ということで、めでたくそれに落ち着きました。
さて、この先を作る前に、まず上のsqlexec関数が正しくSQL文を処理できるかどうかを簡単に確認しておきましょう。新規データベースを作るかどうかを尋ねる関数は後で作るとして、上のコードのままではデータベースを作ることができませんので、17行目に「指定データベースが開けなかったら作る」というフラグを追加したいと思います。
let flags = SQLITE_OPEN_READWRITE |
            SQLITE_OPEN_CREATE as c_int;
定数SQLITE_OPEN_READWRITE=「2」とSQLITE_OPEN_CREATE=「4」のビット論理和「|」ですから、普通に「6」になります。よって、「let flags = 」の後に「6」とぽつんと書いただけでも同じことです。
errmsg関数はそのままとし、その下のopentest関数を次のように書き換え、さらにエラーを表示させる関数を追加します。エラー委譲のストップはopentest関数からその関数に移します。main関数まで委譲させてそちらで処理してもいいのですが、main関数はなるべく簡潔にしておきたいので……。
Rust:SQL等をインスタンスに渡して関数を実行 [sqlexec.rs]
pub fn opentest() -> Result<()> {  // Result型を返すよう変更。
    let mut q = Sql::new();

    q.dbname = "test.db".to_string();

    // SQL文は一つのString型変数に全部連続で書き込む。
    // 分解はSQLite3に任せる。
    let mut s = "BEGIN;".to_string();
    s += "DROP TABLE IF EXISTS test;";
    s += "CREATE TABLE test(id INTEGER PRIMARY KEY, name);";
    s += "INSERT INTO test VALUES(1, ?);";
    s += "INSERT INTO test VALUES(?, ?);";
    s += "INSERT INTO test VALUES(?, ?);";
    s += "INSERT INTO test VALUES(?, '水木 なつみ');";
    s += "INSERT INTO test VALUES(?, ?);";
    s += "INSERT INTO test VALUES('6', ?);";    
    s += "SELECT * FROM test;";
    s += "SELECT id, typeof(id), name, typeof(name) FROM test;";
    s += "SELECT * FROM test WHERE name LIKE ?;";
    s += "SELECT * FROM test ORDER BY name;";
    s += "COMMIT;";
    q.sql = s;

    // SQL文のプレースホルダ「?」にはめ込む値。
    // こちらはString型ベクターに一つずつ詰める。
    // このプログラムは番号付きのプレースホルダには対応していないので、
    // はめ込む順番どおりに書く必要がある。
    let valuev = [
        "雛鶴 あい", "2", "川島 緑輝", "3", "緑川 ルリ子", 
        "4", "5", "小糸 侑", "山田 リョウ", "%リ%"
        ].iter().map(|s| s.to_string()).collect();
    q.values = valuev;

    println!("SQLite3に渡すSQL => {}", q.sql);
    let _ = q.sqlexec()?;

    q.datadisplay();

    Ok(())
}

pub fn callfn() {
    let res = opentest();
    match res {
        Ok(_) => println!("無事完了!"),
        Err(e) => eprintln!("エラー!:{}", e),
    }
}
そして、main.rsのmain関数を下記のように書き換えてからcargo runします。
Rust:SQL実行関数を呼ぶ [main.rs]
mod func;

fn main() {
    func::sqlexec::callfn();
}
実行結果
SQLite3に渡すSQL => BEGIN;DROP TABLE IF EXISTS test;CREATE TABLE test(id INTEGER PRIMARY KEY, name);INSERT INTO test VALUES(1, ?);INSERT INTO test VALUES(?, ?);INSERT INTO test VALUES(?, ?);INSERT INTO test VALUES(?, '水木 なつみ');INSERT INTO test VALUES(?, ?);INSERT INTO test VALUES('6', ?);SELECT * FROM test;SELECT id, typeof(id), name, typeof(name) FROM test;SELECT * FROM test WHERE name LIKE ?;SELECT * FROM test ORDER BY name;COMMIT;
実行中のSQL文 = BEGIN;
実行中のSQL文 = DROP TABLE IF EXISTS test;
実行中のSQL文 = CREATE TABLE test(id INTEGER PRIMARY KEY, name);
実行中のSQL文 = INSERT INTO test VALUES(1, ?);
実行中のSQL文 = INSERT INTO test VALUES(?, ?);
実行中のSQL文 = INSERT INTO test VALUES(?, ?);
実行中のSQL文 = INSERT INTO test VALUES(?, '水木 なつみ');
実行中のSQL文 = INSERT INTO test VALUES(?, ?);
実行中のSQL文 = INSERT INTO test VALUES('6', ?);
実行中のSQL文 = SELECT * FROM test;
実行中のSQL文 = SELECT id, typeof(id), name, typeof(name) FROM test;
実行中のSQL文 = SELECT * FROM test WHERE name LIKE ?;
実行中のSQL文 = SELECT * FROM test ORDER BY name;
実行中のSQL文 = COMMIT;
1 雛鶴 あい    // INSERT結果確認。
2 川島 緑輝
3 緑川 ルリ子
4 水木 なつみ
5 小糸 侑
6 山田 リョウ
1 integer 雛鶴 あい text    // 各データの型も表示。
2 integer 川島 緑輝 text
3 integer 緑川 ルリ子 text
4 integer 水木 なつみ text
5 integer 小糸 侑 text
6 integer 山田 リョウ text
3 緑川 ルリ子    // 名前に「リ」が含まれる者。
6 山田 リョウ
5 小糸 侑        // 名前による昇順。
6 山田 リョウ
2 川島 緑輝
4 水木 なつみ
3 緑川 ルリ子
1 雛鶴 あい
無事完了!
★リョウのid番号、SQL文では「6」をシングルコーテーションで囲んで文字列で指定したのに、型がintegerになっています。これはテーブルを作るときに「id INTEGER PRIMARY KEY」で整数値を指定したからです。このようなカラムが対象でなければ普通に文字列としての「6」が入ったはずです。
正しく処理されました。プレースホルダへのバインドも問題ないようです。「実行中のSQL文」の箇所では、構造体インスタンスに連続した文字列で渡した上のSQL文が、正確に切り分けられているのが見て取れます。まあ最後の「昇順」だけは「ん?」という結果ですが、漢字の文字コードで並べ替えているので仕方がありません。もし五十音順で正しく並べたければ、かな表記列を用意して、そちらを基準にして並べ替えるしかありません。
★上のように境目が分かりやすいパターンではなく、文字列としてのセミコロンやシングルコーテーションが、デリミタや引用符としてのそれらとぐちゃぐちゃに入り乱れて使われていたとしても、各文がSQL文として成立してさえいれば、SQLite3はSQL文の切り分けを難なくやってのけます。その実験はC++版の方でやっています。

C++のコードをRust用に書き直す:その2

では、残りを再現してしまいましょう。SQLファイルを読み込む関数と、処理前に簡単な確認をする関数です。前者には簡単なエスケープ機能を付けます(C++版参照)。これらのコードは、今回初めてtools.rsファイルに記載します。
Rust:Sqlファイルを読み込む関数と確認関数 [tools.rs]
use std::fs::File;
use std::io::{BufRead, BufReader,};

use super::Sql;

type Result = 
    std::result::Result<T, Box<dyn std::error::Error>>;

impl Sql {
    // データベース名、sql、バインド値を書いたファイルを読み込む関数。
    pub fn sqlfileread(&mut self) -> Result<()> {
        // フラグ初期化。複数の変数の初期化を1行で書くときはこうする。
        let (mut kflg, mut oflg, mut flg) = (-1, 1, 0);
        // 構造体格納のファイル名によってファイルオープン。
        let file = File::open(&*(self.sqlfile))?;
        // バッファに読み込み、1行ずつ取り出し、さらに1文字ずつ検証。
        let text = BufReader::new(file);
        for line in text.lines() {
            let l = line?;
            // sは有効な文字を追記してゆく変数。
            let mut s = String::new();
            // 行が以下の文字列であったらフラグを立て、上に戻る。
            if      l == "//--file" {kflg = 0; continue;}
            else if l == "//--sql"  {kflg = 1; continue;}
            else if l == "//--val"  {kflg = 2; continue;}
            else if l == "//--0"  {oflg = 0; continue;}
            else if l == "//--1"  {oflg = 1; continue;}
            // 行が上の文字列ではなく、oflgが1ならば、下の処理に進む。
            if oflg == 0 {continue;}
            // .chars()で文字列を文字に分けてイテレートし、
            // 出現した文字に応じてフラグを操作。
            for c in l.chars() {
                if flg == 0 {
                    if c == '/' {flg = 1;}
                    else if c == '\\' {flg = -1;}
                    else {s.push(c);}
                } else if flg == 1 {
                    if c == '/' {flg = 2; break;}
                    else if c == '*' {flg = 3;}
                    else {flg = 0; s.push('/'); s.push(c);}
                } else if flg == 3 {
                    if c == '*' {flg = 4;}
                } else if flg == 4 {
                    if c == '/' {flg = 0;}
                    else {flg = 3;}
                } else if flg == -1 {
                    if c == '/' {flg = 0; s.push(c);}
                    else if c == '\\' {s.push(c);}
                    else {flg = 0; s.push('\\'); s.push(c)}
                }
            }
            // 3と4は「/*」が成立している場合で、これは改行後も
            // 有効なので、フラグを0に戻さないようにする。
            if flg != 3 && flg != 4 {flg = 0;}
            // 構造体フィールドに書き込む。
            // SQL文だけは追記。下記のように「+=」で追加するのなら
            // sを&str型にする必要がある。
            if !s.is_empty() {
                if kflg == 0 {self.dbname = s;}
                else if kflg == 1 {self.sql += &s;}
                else if kflg == 2 {self.values.push(s);}
            } else {
            // kflgが2、すなわちバインド値エリアにおいて、
            // 行の文字列が「//--blank」であるならば、
            // 空文字列をバインド文字列格納ベクターに加える。
            // 改行のみの行は無視するので、空文字列が必要ならこれを使う。
                if kflg == 2 && l == "//--blank" {
                    self.values.push(String::new());
                }
            }
        }
        Ok(())
    }

    // SQL処理前の簡単な確認関数。
    pub fn sqlvalid(&self) -> Result<()> {
        if self.dbname.is_empty() {
            let emsg = "ファイル名が空白です。";
            return Err(emsg.into());
        }
        
        if self.sql.is_empty() {
            let emsg = "SQL文がありません。";
            return Err(emsg.into());
        }

        let mut qcnt = 0;
        for c in self.sql.chars() {
            if c == '?' {qcnt += 1}
        }
        if self.values.len() != qcnt {
            let emsg = format!("{}", "?と値の数が合いません。");
            return Err(emsg.into());
        }

        Ok(())
    }
}
テストに使った関数opentest()とcallfn()は、もう使わないのでsqlexec.rsから削除します。代わりに、func.rsの方に次のようなコードを追加します。
Rust:Sqlファイルを読み込んで実行する関数 [func.rs]
pub mod sqlexec;
pub mod tools;

// ↓新たに書き入れる。
type Result<T> = 
    std::result::Result<T, Box<dyn std::error::Error>>;

// 変更なし。
struct Sql {
    sqlfile: String,
    dbname: String,
    sql: String,
    values: Vec<String>,
    resultdata: Vec<Vec<String>>,
}

// 変更なし。
impl Sql {
    pub fn new() -> Sql {
        Sql {
            sqlfile: String::from("sqlval.txt"),
            dbname: String::new(),
            sql: String::new(),
            values: Vec::new(),
            resultdata: Vec::new(),
        } 
    }
}

// ここから下は新たに書き入れるもの。
// 構造体のインスタンス生成とSQL処理メソッドを呼ぶ関数。
// エラー委譲はここで止め、main関数には持ち越さない。
pub fn dosqlfn() {
    let mut q = Sql::new();
    let res = q.sqlcontexec();
    match res {
        Ok(_) => (),
        Err(e) => println!("{}", e),
    }
}

// SQL処理メソッド。いちばん下の結果表示メソッドは、
// ほぼエラーは起こらないと見てResultを返す関数にはしなかった。
impl Sql {
    pub fn sqlcontexec(&mut self) -> Result<()> {
        self.sqlfileread()?;
        self.sqlvalid()?;
        self.sqlexec()?;
        self.datadisplay();
        Ok(())
    }
}
main関数も書き換えます。
fn main() {
    func::dosqlfn();
}
UTF-8のSQL用テキストファイルを用意します。プロジェクトフォルダの一層目、データベースファイルがある層に、ファイル名を「sqlval.txt」として、中に次のように書き入れてから保存します。コンストラクタもどきでデフォルトのSQLファイル名をこのように設定したので、何も指定しなければこの名前のファイルを読みに行きます。このように別ファイルから読み込む場合は、プレースホルダへのバインド値(//--valから下)の文字列をシングルコーテーションで囲む必要はありません。それに対し、SQL文の方では普通に文字列を囲む必要があります。この例のSQL文ではプレースホルダを使ったり使わなかったりしていますが、使わないで直にSQL文に書き込む場合は、「'%山%'」「'*緑*'」というように値をシングルコーテーションで囲んでいます。
Sqlファイル [sqlval.txt]
//--file
test.db

//--sql
BEGIN;
UPDATE test SET name = REPLACE(name, ?, ?) WHERE id = 4;
SELECT * FROM test WHERE name LIKE '%山%';
SELECT * FROM test WHERE name GLOB ? AND name GLOB '*緑*';
SELECT * FROM test WHERE name LIKE '___あ%';
SELECT * FROM test LIMIT ? OFFSET 4;
SELECT count(?) FROM test;
SELECT datetime(CURRENT_TIMESTAMP, ?);
COMMIT;
// ↑SQL文に直接値を書く場合はシングルコーテーションが必要。
// プレースホルダ「?」にはシングルコーテーションを付けないこと。
// 「___あ%」は、「半角アンダーバー3本」+「あ%」。

//--val
水木
山口
*川*
1
id
localtime
// ↑バインド値として書く場合はシングルコーテーション不要。
// 文字としてのシングルコーテーションをエスケープする必要がないので楽。
実行する前にもう一つ。データベースを作るフラグと確認用のコードを削除しておきましょう。SQLITE_OPEN_CREATEフラグが立っていると、指定したデータベースファイルが存在しない場合は新規に作られてしまうので、既存のデータベースファイルだけを扱いたいのならこのフラグは不要です。sqlexec.rsのsqlexec関数のうち、データベースオープン処理内の「flags」変数を次のように、いちばん最初の状態に戻します。
let flags = SQLITE_OPEN_READWRITE as c_int;
同じく、プリペアドステートメント処理内の次の行を削除するかコメントアウトします。
// 現在処理中のSQL文の表示。確認用なので後で消す。
// let c_c_char = unsafe {sqlite3_sql(stmt)};
// let slice = unsafe {CStr::from_ptr(c_c_char)};
// println!("実行中のSQL文 = {}", slice.to_str()?);
ここまでできたら、cargo runしてみましょう。
実行結果
4 山口 なつみ     // 置換Update → 名前に「山」を含む。
6 山田 リョウ     // 名前に「山」を含む。
2 川島 緑輝       // 名前に「川」および「緑」を含む。
3 緑川 ルリ子     // 名前に「川」および「緑」を含む。
1 雛鶴 あい       // 名前の4文字目に「あ」を含む。
5 小糸 侑         // 5番目以降から一つだけ表示。
6                // idカラムのデータ数、すなわちレコード数。
2023-09-19 16:44:18    // ローカルタイム。日本時間。
わざとらしく6人全員が1回ずつ表示されるようなSQLにしましたが、思惑どおり全員が正しく表示されました。その下の関数系のコマンドも大丈夫なようです。
次に、データベースファイルを新規に作るかどうかを対話形式で尋ねる関数を再現します。これはtools.rsファイルに書くことにします。
Rust:新規データベースを作るかどうかを尋ねる関数 [tools.rs]
// useをいろいろ書き加える必要がある。
use std::fs::File;
use std::ffi::{CString, c_int};
use std::ptr;
use std::io::{
    BufRead, BufReader,
    stdin, stdout,
    Write
};

use sqlite3_sys::*;

use super::Sql;
use super::sqlexec::errmsg;

type Result<T> = 
    std::result::Result<T, Box<dyn std::error::Error>>;

impl Sql {

// (sqlfileread関数は変更なし)

    pub fn sqlvalid(&self) -> Result<()> {
        if self.dbname.is_empty() {
            let emsg = "ファイル名が空白です。";
            return Err(emsg.into());
        }

        // ここ↓に新しい関数コールを挿入。
        dbexists(&self.dbname)?;
        
        if self.sql.is_empty() {
            let emsg = "SQL文がありません。";
            return Err(emsg.into());
        }

        let mut qcnt = 0;
        for c in self.sql.chars() {
            if c == '?' {qcnt += 1}
        }
        if self.values.len() != qcnt {
            let emsg = format!("{}", "?と値の数が合いません。");
            return Err(emsg.into());
        }

        Ok(())
    }
}

// 以下、対話形式の新規データベースファイル作成関数を追加。
// エラーの場合はそこで処理が中断して、
// Errアーム内のメッセージは呼び出し元に移譲されるが、
// Okは止まらず次の処理に進んでいくため、
// Okの場合のメッセージはその場で表示する。
pub fn dbexists(dbname: &str) -> Result<()> {
    let c_filename = CString::new(dbname)?;
    let mut db = ptr::null_mut();
    let flag: c_int = SQLITE_OPEN_READONLY;
    let rc = unsafe {
        sqlite3_open_v2(
            c_filename.as_ptr(), &mut db, flag, ptr::null()
        ) as i32
    };
    // READONLYで開いてみて、開くことができたら閉じてOk(())を返す。
    if rc == SQLITE_OK {
        let _ = unsafe {sqlite3_close_v2(db) as i32};
        return Ok(());
    } else {
        // 開けなかった場合はユーザーに判断を委ねる。
        println!("{}", "データベースを開けません。\n\
                        新しいファイルを作りますか?");
        loop {
            print!("{}", "y/n:");
            // print!の場合、flushですぐに出力しておかないと、
            // 入力→上記print!→この直近の文字列
            // という順番で出力されてしまう。
            stdout().flush()?;
            let mut y_or_n = String::new();
            stdin().read_line(&mut y_or_n)?;
            // 入力文字列の比較はtrim()を付けないとうまくいかない。
            // 改行コードが邪魔になるからである。
            // trim()は前後の空白を削除する関数で
            // 文字列スライスを返すので型は&strとなり、
            // 改行コードも削除されるため、"y"などと比較できるようになる。
            if y_or_n.trim() == "y" {
                let flags: c_int = SQLITE_OPEN_READWRITE | 
                                   SQLITE_OPEN_CREATE;
                let rc = unsafe {
                    sqlite3_open_v2(
                        c_filename.as_ptr(), &mut db, flags, ptr::null()
                    ) as i32
                };
                let res = errmsg(rc, "sqlite3_open_v2", db);
                if res.is_err() {
                    break;
                } else {
                    println!("{}", "新しいファイルを作りました。");
                    let rc = unsafe {sqlite3_close_v2(db) as i32};
                    errmsg(rc, "sqlite3_close_v2", db)?;
                    return Ok(());
                }
            } else if y_or_n.trim() == "n" {
                break;
            } else {
                println!("{}", "半角のyかnを入力してください。\n\
                                新しいファイルを作りますか?");
            }
        }
        let emsg = "データベースファイルは作成されませんでした。";
        return Err(emsg.into());
    }
}
今加えた関数のテストをしましょう。例によって、わざと入力キーを誤ったり、新規作成をためらったりしてみますが、その前にsqlval.txt内のデータベースファイル名を適当に変え、//--0を書き入れてSQL文から下は読み込まないようにしてから実行します。
Sqlファイル [sqlval.txt]
//--file
testtest.db
// ↑ファイル名を適当に変える。
// ↓//--0を書き入れてその下をエスケープする。
//--0
//--sql
BEGIN;
UPDATE test SET name = REPLACE(name, ?, ?) WHERE id = 4;
SELECT * FROM test WHERE name LIKE '%山%';
SELECT * FROM test WHERE name GLOB ? AND name GLOB '*緑*';
SELECT * FROM test WHERE name LIKE '___あ%';
SELECT * FROM test LIMIT ? OFFSET 4;
SELECT count(?) FROM test;
SELECT datetime(CURRENT_TIMESTAMP, ?);
COMMIT;

//--val
水木
山口
*川*
1
id
localtime
実行結果
データベースを開けません。
新しいファイルを作りますか?
y/n:no
半角のyかnを入力してください。
新しいファイルを作りますか?
y/n:123
半角のyかnを入力してください。
新しいファイルを作りますか?
y/n:
半角のyかnを入力してください。
新しいファイルを作りますか?
y/n:だって迷ってるんだもの……
半角のyかnを入力してください。
新しいファイルを作りますか?
y/n:n
データベースファイルは作成されませんでした。

// (もう一度cargo run)

データベースを開けません。
新しいファイルを作りますか?
y/n:y
新しいファイルを作りました。
SQL文がありません。
大丈夫そうですね。ためらったあげくに結局「y」を押していますので、プロジェクトフォルダの中には「testtest.db」という空っぽのデータベースファイルが生成されているはずです。それは不要ですので、ごみ箱に捨てておきます。

C++のコードをRust用に書き直す:その3

最後に、SQL処理関数がさまざまな引数に対応できるようにしておきたいと思います。と言っても、Rustでは関数のオーバーロードはできないとのことなので、「引数違いの同名関数」に似た機能は、マクロで実現することになります。また、マクロなら、println!などからも察せられるように、可変数引数にも対応できます。
流れとしては、①mainから同名で引数別のマクロを呼ぶ→②それぞれのマクロが対応する関数を呼ぶ→③sqlexec実行、となります。この順で作っていきましょう。まずマクロですが、これは「the book」「19.5.マクロ」に解説とサンプルがあるものの、正直私にはよく分からない記述の方が多くて難儀しました。ともあれ、見よう見まねで書いたのが下のコードです。記述場所はfunc.rsにしました。
Rust:マクロのオーバーロード [func.rs]
#[macro_export]    // ←これでcrateで宣言されたものとして公開になるらしい。
macro_rules! dosql {
    () => {              // 引数なし。デフォルトSQLファイルで処理。
        func::dosql_read_txt1();
    };
    ($var: expr) => {    // 引数1個。SQLファイルを指定して処理。
        func::dosql_read_txt2($var.to_string());
    };
    ($var1: expr, $var2: expr) => {  // 引数2個。ファイル名+SQL文で処理。
        func::dosql_no_values($var1.to_string(), $var2.to_string());
    };
    ($var1: expr, $var2: expr, $($val: expr),+) => {{   // 引数3個以上。
        let mut tmp = Vec::new();        // ファイル名+SQL文+値で処理。
        $(tmp.push($val.to_string());)+  // +は1回以上の繰り返し。
        func::dosql_has_value($var1.to_string(), $var2.to_string(), tmp);
    }};
}
$varは変数名、exprは「式、値」を意味するフラグメント指定子です。引数2個までは、読みづらいけれどもやっていることは単純なので、そんなに難しくはありません。上から順に進み、引数の数が合ったらそれらをString型に変換して、対応する関数に渡しているだけです。分かりにくいのはいちばん下の、引数が3個以上あった場合の処理です。繰り返し記号「+」がある2箇所を分析的に見てみると、
●引数:var1、var2、[val,]の1回以上の繰り返し……つまり引数が3個以上の場合は
●処理:[tmp.push(val.to_string())]の1回以上繰り返し……つまり3番目以降の引数をベクターtmpにpushする処理を繰り返してから、それ(tmp)をvar1、var2とともに3個目の引数として関数に渡す。
となるでしょう。正直、第3引数以降をベクターにpushしている箇所では「+」の後に「;」を打たないことや、この処理部分のみ{{ }}と二重括弧で囲まないとエラーになってしまうことには、いまだにピンときていません。
★上のコード、=> の右側の処理の部分はいずれも、{ }でなくても、( )や[ ]で囲んでもエラーになりません。しかし、最後の二重括弧の部分のみ、内側の括弧は{ }でないとダメなのです。繰り返しを含む場合は{ }で囲まなければいけないとか何とか、何かしら規則があるのでしょうが、今のところ調べがついていないので、「こういうものなんだ」と思って進めるしかありません。
次に、マクロが引数に応じて呼ぶ関数を作ります。そこで構造体のインスタンスを作って各引数をフィールドに渡し、SQL処理が実行できる形にしてからSQL実行関数を呼びます。
Rust:マクロから引数に応じて呼ばれる各関数 [func.rs]
// この下の関数は呼ばれない可能性があるため、
// #[allow(dead_code)]を付けて使わなくても警告が出ないようにしておく。
// ファイル名やSQL文の最後にスペースが入るとエラーになることがある。
// よって、変数格納時にtrim()で余計なスペースを削除する。

// 引数なし→デフォルトの「SQLファイル名」で実行。
#[allow(dead_code)]
pub fn dosql_read_txt1() {
    let mut q = Sql::new();
    q.dosqlfn(true);  // SQLファイルを読む場合は「true」、
}                     // 読まない場合は「false」を渡す。

// 引数1個→引数「SQLファイル名」で実行。
#[allow(dead_code)]
pub fn dosql_read_txt2(var: String) {
    let mut q = Sql::new();
    q.sqlfile = var.trim().to_string();
    q.dosqlfn(true);
}

// 引数2個→第1引数「データベース名」、第2引数「SQL文集」で実行。
#[allow(dead_code)]
pub fn dosql_no_values(var1: String, var2: String) {
    let mut q = Sql::new();
    q.dbname = var1.trim().to_string();
    q.sql = var2.trim().to_string();
    q.dosqlfn(false);
}

// 引数3個以上→第1引数「データベース名」、第2引数「SQL文集」、
// 第3引数「プレースホルダ用値格納ベクター」で実行。
#[allow(dead_code)]
pub fn dosql_has_value(var1: String, var2: String, val: Vec) {
    let mut q = Sql::new();
    q.dbname = var1.trim().to_string();
    q.sql = var2.trim().to_string();
    q.values = val;
    q.dosqlfn(false);
}
最後に、上で書いた実行関数に手を入れます。main関数は、マクロを通じてfuncグループ内のSQL実行関数にアクセスできるようになったので、funcグループをより閉鎖的にします。
Rust:SQL実行関数を呼ぶ関数 [func.rs]
// dosqlfn関数をSql構造体のメソッドに変更。
// main関数からは呼べなくなるが、マクロがあるのでそちらを使う。
impl Sql {
    pub fn dosqlfn(&mut self, rb: bool) {
        let res = self.sqlcontexec(rb);
        match res {
            Ok(_) => println!("正常終了しました!"),
            Err(e) => println!("{}\n{}", e, "処理を中止しました!"),
        }
    }

    // sqlfileread関数はSQLファイルを読み込むときのみ、
    // すなわち引数rbがtrueのときだけ使う。
    pub fn sqlcontexec(&mut self, rb: bool) -> Result<()> {
        if rb {self.sqlfileread()?;}
        self.sqlvalid()?;
        self.sqlexec()?;
        self.datadisplay();
        Ok(())
    }
}
func.rsの「pub」も消しておきます。これでもうmain関数は、マクロを通す以外にfuncグループには触れません。
mod sqlexec;    // 両方とも「pub」を削除。
mod tools;

SQL実行プログラムのテスト

それでは、ちゃんと動くかどうかテストしてみましょう。まず引数2個をdosqlマクロに渡します。第1引数はデータベース名、第2引数はSQL文(複数を連続して書いても可)です。main関数に次のように書き入れて実行します。
Rust:テスト~引数2個の場合 [main.rs]
fn main() {
    // &str文字列をそのまま渡す。
    dosql!("test.db", "SELECT * FROM test;");

    // String型変数に格納してから渡す。
    let dbname = "test.db".to_string();
    let sql = "SELECT * FROM test WHERE id = 6;".to_string();
    dosql!(dbname, sql);

    // &str文字列のSQL文の連続を変数に格納して渡す。
    let sql = "SELECT * FROM test WHERE id = 5;\
               SELECT * FROM test WHERE id = 4;\
               SELECT * FROM test WHERE id = '3';\
               SELECT * FROM test WHERE name LIKE '%輝';\
               SELECT * FROM test WHERE name GLOB '雛鶴*';";
    dosql!(dbname, sql);
}
実行結果
1 雛鶴 あい
2 川島 緑輝
3 緑川 ルリ子
4 山口 なつみ
5 小糸 侑
6 山田 リョウ
6 山田 リョウ
5 小糸 侑
4 山口 なつみ
3 緑川 ルリ子
2 川島 緑輝
1 雛鶴 あい
正常終了しました!
正しい結果が返りました。dosql!マクロに渡す文字列は、&str型でもString型でも大丈夫です。実行前にすべてString型文字列に変換してから実行するからですが、to_string()は、対象がもともとString型文字列であったとしてもエラーにはなりません。また、INTEGER PRIMARY KEY指定のid列は、文字列数字で渡してもSQLite3の方で数値に読み替えます。
次は引数が3個以上ある場合、すなわち可変数引数が処理できるかどうかをテストします。第1引数データベース名、第2引数SQL文集は上と同じで、第3引数以降はプレースホルダにバインドする値を列挙します。まず、なつみを結婚前の姓に戻しましょう。引数は5個になります。
Rust:テスト~引数3個以上の場合 [main.rs]
fn main() {
    // 引数5個。
    dosql!(
        "test.db", 
        "UPDATE test SET name = REPLACE(name, ?, ?) WHERE id = ?;",
        "山口", "水木", 4
    )
}
実行結果
返ったデータはありません。
正常終了しました!
SELECT文を書かなかったので、結果が分かりません。それを確認がてら、次のようなSQLを実行します。引数は一つ増えて6個です。
Rust:テスト~引数3個以上の場合 [main.rs]
fn main() {
    let mut sql = "BEGIN;".to_string();
    sql += "REPLACE INTO test VALUES(?, ?);";
    sql += "SELECT * FROM test WHERE id = ? OR id = ?;";
    sql += "COMMIT;";

    // 引数6個。
    dosql!("test.db", sql, 6, "廣井 きくり", 4, 6);
}
実行結果
4 水木 なつみ
6 廣井 きくり
正常終了しました!
先ほどの改名は正しく実行されていました。リョウときくりの交替もOKです。
次は引数が一つもないか、あるいは1個だけの場合、すなわちSQLファイルを読み込んでの処理のテストです。sqlval.txtの中身を次のように書いて実行します。
Sqlファイル [sqlval.txt]
//--file
test.db

//--sql
BEGIN;
REPLACE INTO test VALUES(?, ?);
INSERT INTO test(name) VALUES(?);
SELECT * FROM test;
COMMIT;

//--val
3
牧村 ミキ
Vivy
Rust:テスト~引数なしの場合 [main.rs]
fn main() {
    dosql!();  // 引数なしならsqlval.txtを読みに行く。
}
実行結果
1 雛鶴 あい
2 川島 緑輝
3 牧村 ミキ
4 水木 なつみ
5 小糸 侑
6 廣井 きくり
7 Vivy
正常終了しました!
大丈夫ですね。次に、新しいSQLファイルを作って読み込ませてみます。新しいテキストファイル「sqlnew.txt」(文字コード:UTF-8)を作成し、プロジェクトフォルダの一層目に置いて、中身を次のようにして実行します。
Sqlファイル [sqlnew.txt] ←新規SQLファイル
//--file
test.db

//--sql
BEGIN;
SELECT * FROM test WHERE name LIKE ?;
SELECT * FROM test WHERE name GLOB ?;
SELECT * FROM test WHERE name GLOB ?;
SELECT * FROM test WHERE name GLOB ?;
COMMIT;

//--val
v%
v*
*[ぁ-んァ-ン]*
* *[^ぁ-んァ-ン]*
Rust:テスト~引数1個の場合 [main.rs]
fn main() {
    dosql!("sqlnew.txt");  // 引数1個ならそのSQLファイルを読みに行く。
}
実行結果
7 Vivy        // LIKEの方がヒット。GLOBは大文字小文字を区別する。
1 雛鶴 あい   // 名前にかな文字を含む4人。
3 牧村 ミキ
4 水木 なつみ
6 廣井 きくり
2 川島 緑輝   // 半角スペースの後にかな文字を含まない二人。
5 小糸 侑
正常終了しました!
★SQLite3のLIKEとGLOBでは、書法が異なるので注意が必要です。例えば、0文字以上の任意の文字列は、LIKEでは「%」、GLOBでは「*」であり、任意の1文字は、LIKEでは「_」、GLOBでは「?」となっています。また、正規表現と違って、「その文字のn回の繰り返し」を表現することはできません。GLOBで任意の4文字を表現したければ「????」と書く必要があります。
大丈夫でした。main関数にデータベース名やSQL文を直接書くやり方では、いちいちビルドをやり直さねばならず、そのたびに2~3秒かかってしまいますが、SQLファイルを読み込ませる形なら、main関数は引数0個または1個のdosqlマクロで固定できますので、ビルドの時間がかからず、上記程度のデータベースとSQL文なら0.01秒で処理できます。とはいえ、このプログラムの大元はほかのデータベース・プログラム(CDデータベース)の部品として作ったもので、そちらではSQLファイルを読み込んで実行するケースはありません。SQLはすべてプログラム内部に書かれた固定的なものであり、新規のクエリが外部から発せられることはないからです。
試しに、そのCDデータベースで使っているSQLite3ファイルを今回のプロジェクトフォルダにコピーして、SQLファイルを読み込む方法でクエリを発行してみます。sqlnew.txtを次のように書き換えて実行しました。
Sqlファイル [sqlnew.txt]
//--file
CDdatabase.sqlite

//--sql
SELECT 整理番号,品番,CDタイトル,識別情報 FROM v_cdlist 
WHERE 整理番号 IN 
(SELECT DISTINCT shinabanid FROM t_cdkyokumoku 
WHERE kyokuid IN 
(SELECT kyokuid FROM t_kyoku WHERE sakkyokukaid IN 
(SELECT sakkyokukaid FROM t_sakkyokuka WHERE sakkyokuka 
GLOB ? ORDER BY sakkyokuka)));

//--val
デュファイ

//--sql
SELECT B.sakkyokukaid,B.sakkyokuka,A.kyokuid,A.kyoku 
FROM t_kyoku A 
LEFT JOIN t_sakkyokuka B ON A.sakkyokukaid=B.sakkyokukaid 
WHERE B.sakkyokuka GLOB ? AND A.kyoku GLOB ? 
ORDER BY B.sakkyokuka,A.kyoku;

//--val
*ヴェ*
*協奏曲*

//--sql
SELECT * FROM v_cdlist;
実行結果
// 作曲者名「デュファイ」の作品を含むCD。
808 PROA73/7 デュファイ/世俗音楽全集 ロンドン中世アンサンブル
815 POCA1141 ポメリウムIV祈りの時のための音楽 ポメリウム
816 KKCC9068 ヨーロッパ音楽夢街道フランドル クレマンシックほか
818 PROA21/3 ハート型のシャンソン集 コンソート・オブ・ミュージック
821 094638581123 中世・ルネサンスの楽器 マンロウ
// 名前に「ヴェ」を含む作曲家の「協奏曲」。
291 ジョリヴェ 2973 オンド・マルトノ協奏曲
291 ジョリヴェ 3857 コンチェルティーノ(トランペット協奏曲第1番)
291 ジョリヴェ 2970 チェロ協奏曲第1番
291 ジョリヴェ 3856 トランペット協奏曲第2番
291 ジョリヴェ 2030 ハープ協奏曲
291 ジョリヴェ 2969 ピアノ協奏曲
291 ジョリヴェ 5869 ファゴット、ハープ、ピアノと弦楽のための協奏曲
291 ジョリヴェ 5864 フルートと打楽器のための協奏的組曲(フルート協奏曲第2番)
291 ジョリヴェ 4185 フルート協奏曲第1番
291 ジョリヴェ 4336 打楽器と管弦楽のための協奏曲
13 ベートーヴェン 7142 ピアノ協奏曲ニ長調作品61a(ヴァイオリン協奏曲から作曲者編曲)
13 ベートーヴェン 4208 ピアノ協奏曲第1番ハ長調作品15
13 ベートーヴェン 1694 ピアノ協奏曲第2番変ロ長調作品19
13 ベートーヴェン 1838 ピアノ協奏曲第3番ハ短調作品37
13 ベートーヴェン 4210 ピアノ協奏曲第4番ト長調作品58
13 ベートーヴェン 4211 ピアノ協奏曲第5番変ホ長調作品73「皇帝」
13 ベートーヴェン 5140 ピアノ協奏曲第5番変ホ長調作品73「皇帝」より第3楽章
13 ベートーヴェン 6108 ヴァイオリン協奏曲ニ長調作品61
13 ベートーヴェン 1546 三重協奏曲ハ長調作品56
38 ラヴェル 235 ピアノ協奏曲ト長調
38 ラヴェル 236 左手のためのピアノ協奏曲ニ長調
115 ヴェレシュ 1284 クラリネット協奏曲
// 登録済みのクラシックCD全部。
1 徳間ジャパン ドイツ・シャルプラッテン 35TC13/5 バッハ/クリスマス・オラトリオ フレーミヒ 声楽曲 1983-11-25 3枚組。初めて買ったCD。
2 BMGファンハウス RCA BVCC31062 アイヴズ作品集/アメリカン・ジャーニー トーマス 声楽曲 2002-03-20
3 EMIミュージック・ジャパン EMI TOCE90155 ラフマニノフ/交響曲第2番 ラトル 交響曲 2010-09-08
4 ユニバーサルミュージック グラモフォン UCCG4267 ヘンデル/序曲集 リヒター 管弦楽曲 2007-12-12
5 ユニバーサルミュージック グラモフォン UCCG5239 ヘンデル/水上の音楽/王宮の花火の音楽 クーベリック 管弦楽曲 2012-05-09
6 ポリドール グラモフォン F35G50257 バルトーク/管弦楽のための協奏曲 カラヤン旧 管弦楽曲 1985-11-25

// (中略)

1801 フォンテック フォンテック EFCD4082/3 ソナチネアルバム1 神野明ほか 器楽曲 2004-06-21 2枚組。全音の旧ソナチネアルバム第1集の音源集。当の楽譜にもブックレットにも詳しい楽曲説明がないので、特に2枚目は元曲を割り出すのが大変だった。
1802 日本アコースティックレコーズ NAR(日本アコースティックレコーズ) NARD5008/9 ソナチネアルバム1(新) 今井顕ほか 器楽曲 2004-06-21 2枚組。全音の新ソナチネアルバム第1集の音源集。「初版および初期楽譜に基づく校訂版」とある。
正常終了しました!
上の三つのクエリで、ヒットしたレコードが全部で1830行ほど表示されましたが、これでも所要時間は依然として0.01秒のままでした。最後のクエリにORDER BYを加えてレーベル順とかCDタイトル順とかで1802レコードを並べ替えても、所要時間0.01秒はまったく変わりません。この程度の大きさのデータベースでは、SELECTで何を問い合わせても所要時間に差は出ないようです。これがUPDATEやDELETE等の更新系のクエリだったらもっと時間がかかるのかもしれませんが、いずれにせよ、そういうのはSQLite3の問題です。データベース操作におけるレスポンスという点では、C++版にもRust版にもまったく不満はありません。
★SQLite3のレスポンスについては、なでしこやPythonを使ったときも、特に不満はありませんでした。
エラー時の反応もざっと確認しました。
実行結果
// SQLファイルがなかった場合。
指定されたファイルが見つかりません。 (os error 2)
処理を中止しました!
// データベースファイル名を書かなかった場合。
ファイル名が空白です。
処理を中止しました!
// SQL文を書かなかった場合。
SQL文がありません。
処理を中止しました!
// プレースホルダと値の数が異なる場合。
?と値の数が合いません。
処理を中止しました!

// SQL文が誤っていた場合。
sqlite3_prepare_v2 のエラー:near "SELECT": syntax error
処理を中止しました!
// SQL文は正しいが、COMMITだけでBEGINがないなど
// 実行時にエラーになった場合。
5 文目でエラー発生!
sqlite3_step のエラー:cannot commit - no transaction is active
処理を中止しました!
いちばん上、SQLファイルがなかったときのメッセージが日本語であるということは、これはRustではなくOSが返したメッセージということでしょうか英語のエラーメッセージを予想したので驚きましたが、日本語でも別に問題はない、というかむしろ歓迎です。

ご意見・ご教示等ございましたら こちら からお送りください。

Copyright © 2023 鷺澤伸介 All rights reserved.