3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[VBA/Rust]VBAからRustを使う Part1

3
Last updated at Posted at 2025-12-05

[VBA/Rust]VBAからRustを使う Part1

概要

初投稿ですが非常にニッチで需要が少ない内容で記事を作ってみました。
これはRustで静的にxlsxファイルを操作するものではなく、VBAからRustの処理を呼び出して、
VBAの一部のように使用するためのテクニックです。

目的

最近ExcelではPython in ExcelxlwingsなどPythonを使えるサービスもありますが、
有償だったりクラウド実行でデータが社外に送信されたり、またマクロブック配布先にPython環境が必要だったりとVBAと比較してそれぞれ制約があります。

またopenpyxlpandasなどで外部から静的にExcelブックを操作することもできますが、
Excelを開いたままで即反映されたりするわけではなく静的なプロセスになるため、こちらも運用状況によっては適切でない場合もあります。

そこでVBAからdllを呼び出して重いロジックなどを担当させることで、VBAが苦手とするパフォーマンスの部分を補うことが本記事の題材の主目的です。
計算処理だけならこのようなテクニックはあまり必要に感じられないかもしれませんが、例えばシートに100万行あるデータを展開するのも
今回の方法を応用すれば一瞬で終わります。しかもExcelを開けたままで即セルに反映されます。(本記事で作るものではありませんが...)

なぜRustか

  • モダン
  • 低水準操作に適している
    • C++よりもメモリ安全性が高い
  • C# COMよりもパフォーマンスが優れている

目次

アーキテクチャと概念解説

VBAとRustはそれぞれ異なる言語であるため、ランタイムメモリの管理権のルールデータの並び方がまったく違います。
ランタイムについては、RustはCPUが直接理解できる機械語(ネイティブコード)で動くのに対して、
VBAはC#やJavaと同じく中間言語にコンパイルされ仮想マシン上で動きます(仕組みは異なりますが)。
メモリの管理権はGCの有無に関係します(厳密にいうとVBAにGCはなく参照カウント方式ですが)。
GCのある言語ではメモリ管理権限はGCにあり、GCが勝手にメモリアドレスの番地を変えることもあります。
データの並び方については、VBAはPythonや他の高水準言語と同じく、1つのデータはメタデータのようなものをたくさん持っており、
RustやCのようなシンプルなデータの並び方とは異なります。

上記の違いを吸収するため、それぞれが共通のルールで対話できるようにする必要があります。
それを実現するのが、VBAとRust双方が理解できるC ABIです。
これはFFI(Foreign Function Interface)という仕組みで、実際に使用するプロトコルがC ABIです。

ABI(Application Binary Interface)API(Application Programming Interface)と一字違いで似ていますが、
ABIは0と1の並び方の規約となるため、APIより低水準の話になります。

C ABIはC言語のためのABIですが、歴史上あらゆる言語の標準語になっています。
ある意味英語のような存在といえるかもしれません。
つまり、VBAとRustがこの共通のルールに準拠して対話するわけです。

アーキテクチャ

上で少し泥臭い話をしましたが、実際の構成としてはシンプルで、VBA側はWindows APIを呼ぶのとあるていど同じような感覚でコードを書けます。

  1. VBA側でWindows APIをVBAから使用する際によく出てくるDeclareでdllのどの関数を使うか宣言します。
  2. VBAがデータを渡す際、C言語互換のプリミティブなデータ型に変換します。

    ※Long型など数値はそのまま渡せますが、文字列やオブジェクトなどヒープにデータがあるものはポインタが渡されます。
  3. Rust側でデータを受け取り、ポインタであればそのアドレスのデータをC特有のルールに準拠して読取り、Rustのプリミティブ型に変換します。
  4. Rust側でデータを返す際、C特有のルールに準拠してポインタや数値に変換します。
  5. VBA側ではそのポインタを受け取り、VBAのプリミティブ型へ変換します。

全体.png

VBAからC ABIを介してRustにデータを渡す手順は上図のようなイメージです。

基本

プロジェクト構成

dllファイルを作成するので、プロジェクトはcargo new project-name --libで作成します。
Cargo.tomlは下記を必ず追加します。

[lib]
crate-type = ["cdylib"]

cdylibは"C Dynamic Library"の略です。これを指定することでVBAやPythonなどから呼び出せるdllファイルを作成できます。
これを指定しない場合、Rust専用のライブラリファイル.rlibが作成されることになります。

基本構文

VBAとRust間でデータのやり取りをする場合、Rust側では以下3点が基本構文となります。

  • extern "system"
      これはdllを呼び出す際の規約で、関数の後始末を誰が行うかということを明示します。特に何も考えずテンプレートのようにつけておけばいいものです。

    extern "system" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    

    現代の64bit版Windowsでは特に意識する必要はありませんが、32bit版Windowsであれば、
    extern "C"をつけてしまうとVBA側がメモリの後始末をしないといけなくなるため大変です。
    VBA連携であれば基本的にsystemの方をつけておくのが適切です。

  • #[unsafe(no_mangle)]
    Name Manglingを無効化します。Rustはコンパイル時に関数名にハッシュ値などを付与して一意にします。
    そのままではVBAから関数名が検索できないため関数名を維持する目的でこのアトリビュートをつけます。

    #[unsafe(no_mangle)]
    extern "system" fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    

    この場合addという関数名がそのままVBAで使えます。

    no_mangleunsafe属性をつけるようになったのはRust 1.82からです。
    前のバージョンでは#[no_mangle]でないとエラーになる可能性があります。

  • #[repr(C)]
    Rustの構造体や列挙型をC言語の構造体や列挙型に変換するためのアトリビュートです。
    これがないとRustの構造体や列挙型のメモリレイアウトがC言語のものと異なるため、
    VBAから正しくアクセスできないことがあります。

    #[repr(C)]
    struct MyStruct {
        a: i32,
        b: f64,
    }
    

実践1プリミティブ型の受け渡し

まずは基本となる数値(整数・浮動小数点)の受け渡しから始めます。
これらはメモリ上の値をそのままコピーして渡す「値渡し」となるため、メモリ管理の複雑さがなく、最も安全に連携できます。

VBAとRustの型対応表

VBAとRustでは、同じ「整数」でもビット数が異なる場合があります。ここを間違えると、計算結果がおかしくなったり、Excelがクラッシュしたりします。
特に注意すべきは VBAの Long は 32bit である という点です(64bitではありません)。

VBAの型 ビット数 Rustの対応型 備考
Byte 8-bit u8 0 〜 255
Integer 16-bit i16 -32,768 〜 32,767
Long 32-bit i32 最もよく使います
LongLong 64-bit i64 ※64bit Excel専用
Single 32-bit f32 浮動小数点(単精度)
Double 64-bit f64 浮動小数点(倍精度)
Boolean 16-bit i16 VBAのTrueは -1 なので数値として受け取るのが安全

足し算関数を作ってみる

実際に、2つの整数を受け取って足し算をする関数を作ってみます。

#[unsafe(no_mangle)]
pub extern "system" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

これでcargo buildしてください。
※32bit版Windowsではビルドコマンドが異なりますが、本記事では現在主流の64bit版Windowsで話を進めます。

ビルドが終了するとプロジェクト内のtarget/debug/内にdllファイルが出力されます。releaseビルドした場合は、target/release/の方に出力されます。
このdllファイルの絶対パスをコピーしておいてください。

次にVBA側の処理を書きます。標準モジュールなどに下記のように記述します。

' dllパスを指定する
#If Win64 Then
  Private Declare PtrSafe Function rust_add Lib "C:\my_rust_lib.dll" (ByVal a As Long, ByVal b As Long) As Long
#End If

Sub TestRustAdd()
    Dim result As Long
    ' Rustの関数を呼び出す
    result = rust_add(10, 20)

    ' 結果確認: 30 と表示されれば成功!
    MsgBox "Rust計算結果: " & result
End Sub

最初のDeclareのところでdllパスを指定して関数を呼び出し、VBA内部で使う関数名を宣言します。
そしてあとは何も気にせず通常のVBA関数と同じように使うだけです。

下図のように表示されましたか?
このように単純なスタックメモリに値があるだけのものは、値そのものが渡されるためむつかしくありません。

足し算の結果.PNG

bool型の場合も見てみましょう。

#[unsafe(no_mangle)]
pub extern "system" fn rust_judge(bool_raw: i32) -> i32 {
    let bool_val: bool = bool_raw == -1;
    // trueの場合 10 を返し、falseの場合 20 を返す。
    if bool_val { 10 } else { 20 }
}

trueは-1という数値で入ってきます。

VBAは下記のように修正してください。

' dllパスを指定する
#If Win64 Then
  Private Declare PtrSafe Function rust_judge Lib "C:\my_rust_lib.dll" (ByVal bool_raw As Long) As Long
#End If

Sub TestRustAdd()
    Dim result As Long
    ' Rustの関数を呼び出す
    result = rust_judge(True)

    ' 結果確認: 10 と表示されれば成功!
    MsgBox "Rust判定結果: " & result
End Sub

下図のように表示されましたでしょうか?

判定関数の結果.PNG

ここで注意してほしいのはVBA側のDeclare文で必ずByVal(値渡し)を指定することです。
もしByValをつけない場合ByRef(参照渡し)になってしまい、ポインタを渡すことになります。
つまりRust側は数値データとして読み込んだのに、実際はそれは数値データが入っているスタックメモリのアドレスになってしまうわけです。

ByValとByRef.png

実践2文字列の受け渡し(String)

文字列というデータ型は高水準言語では数値などと同じように気軽に扱える存在ですが、
低水準言語、およびそれに近い処理を行う場合、一気に厄介な存在になるものです。
実際Rustでも所有権を意識しだすのは文字列からですし...

データの渡し方の問題

低水準言語を触ったことがある方ならわかると思いますが、単純な数値と文字列はどのメモリに置かれるかが異なります。
数値系は基本的に高速なスタックメモリに配置され、別の変数に代入する場合はコピーされます。

let s = 1;
let s2 = s; // コピー
println!("sはまだ使えます。{}", s);

しかし、文字列型は低速だけどたくさん使えるヒープメモリに確保されます。他の変数に代入する際は基本的に参照を渡すことになります。
下記コードのように

let s = String::from("Hello");
let s2 = s; // 所有権ムーブ(スタックメモリに新たに領域を確保しsが持っていたポインタのアドレスをコピー → sは無効となる)
println!("sもう使えません。{}", s); // コンパイルエラー

これはデータそのものをコピーせずポインタのアドレスをコピーしているため浅いコピーと呼ばれます。
Rustでは所有権というルールで扱っていますが、CやC++でも考え方は同じだと思います。
こういったメモリの配置場所が異なる問題があるだけでなく、エンコーディングの問題も無視できません。

エンコーディングの問題

VBAではUTF-16LEというエンコードを使用し、日本語もアルファベットも基本は2バイトで表現されます。
RustはおなじみのUTF-8を使用し、英語は1バイト、日本語などは3バイト前後の可変長で表現します。
この違いを吸収せずに文字列データを渡すと文字化けやメモリアクセス違反(クラッシュ)を引き起こします。

この問題を解決する一番簡単な方法は、VBAで単純にByVal s as Stringとして渡すことです。
この場合、システムロケールに合わせて文字列が変換され、C言語形式(CStr)の文字列データが入ってきます。
それをRust側でUTF-8に変換すればいいわけです。
しかし、この方法はシステムロケールによって勝手に変換されてしまうため、異なるシステムロケールのPCで実行した場合、文字化けなどのリスクがあります。

そこで、一番手堅い方法として、VBA標準のUTF-16の文字列をそのまま取得してRust内部でUTF-8に変換する流れを採用します。
この場合、VBA側からは文字列そのものではなく、文字列のメモリアドレスとデータの長さを直接Rustに渡しRust側でUTF-16の文字列を復元する流れになります。
ここがシンプルな数値の受け渡しと難易度が大きく異なる点です。

実践 - RustがVBAが確保したメモリを不変参照する

Rust側では下記のように実装してみます。

use ::std::slice;

#[unsafe(no_mangle)]
pub extern "system" fn count_chars(ptr: *const u16, len: i32) -> i32 {
    if ptr.is_null() {
        return 0;
    }

    // ポインタと長さから文字列スライスを復元
    let utf16_slice = unsafe { slice::from_raw_parts(ptr, len as usize) };

    // UTF-16からRustの文字列(UTF-8)に変換
    let rust_string = String::from_utf16_lossy(utf16_slice);

    // 文字数を返す
    rust_string.chars().count() as i32
}

VBA側では下記のように実装します。ポインタとデータの長さを渡しています。
今回はAlias "Rustの関数名"でVBAで使う関数名を別の名前にしてみました。

' dllパスを指定する
# if Win64 Then
  Private Declare PtrSafe Function rust_count_chars Lib "C:\my_rust_lib.dll" Alias "count_chars" (ByVal ptr As LongPtr, ByVal length As Long) As Long
#End If

Sub TestRustAdd()
    Dim s As String
    s = "こんにちわ"

    ' 文字列データのポインタと、文字数を返す
    Dim result As Long
    result = rust_count_chars(StrPtr(s), Len(s))

    MsgBox "文字数: " & result
End Sub

これで下図のように表示されるはずです。
文字数カウント.PNG

このコードではRustはVBAがヒープメモリに展開したデータをスライスとして覗き見ているだけなので、
Rustが文字列を使うといってもメモリの所有権がRust側に移るわけではありません。
つまり、VBA側がこのメモリを管理しているため、こんにちわへの参照を持っているsEnd Subでスコープを抜けて解放されると
ヒープメモリにあるこんにちわのデータも解放されます。

実践 - Rustが確保したメモリをVBAが参照する

次はRustからVBAへ文字列を渡してみたいと思います。こちらは少し注意が必要です。
以下のコードははまだ実行しないでください。

use std::ffi::CString;

#[unsafe(no_mangle)]
pub extern "C" fn get_message() -> *const c_char {
    let s = CString::new("Hello form Rust").unwrap();

    s.into_raw()
}

上記のようにRust側でCStringを使ってヒープメモリに文字列を展開しました。into_rawメソッドを使ってVBA側へポインタを返します。
into_rawメソッドはメモリ管理を放棄してポインタを返すメソッドです。
VBA側ではこれをUTF-8エンコードとして受け取る処理が必要になるのですが、そんなことよりも問題になるのは
Hello form Rustというデータは、get_message関数が終わっても、解放されません。
つまりこのまま放っておいたらメモリリークになります。

そこでRust側にメモリ解放用の関数を用意して、Excel側では処理が終わった後で、必ずその処理を呼ぶ仕組みを作る必要があります。
Rust側で下記のように実装します。通常の関数とCStringで確保したメモリの解放用関数です。

use std::{ffi::CString, os::raw::c_char};

#[unsafe(no_mangle)]
pub extern "C" fn get_message() -> *const c_char {
    let s = CString::new("Hello form Rust").unwrap();
    s.into_raw() // Rust側でメモリ管理を放棄 & ポインタを返す
}

// 文字列専用のメモリ解放関数
#[unsafe(no_mangle)]
pub extern "system" fn free_string(s: *mut c_char) {
    // メモリの2重解放防止
    if s.is_null() {
        return;
    }
    unsafe {
        // ポインタからCStringを復元し、スコープを抜けることで解放させる
        let _ = CString::from_raw(s);
    }
}

VBA側ではRustで確保したメモリを読取り、VBAのString型に変換して使用した後、Rustで用意したfree_stringを実行することでメモリを解放します。
その途中UTF-8からUTF-16への変換処理を挟みますが、それは下記のUtf8PtrToString関数を使用してください。
この関数ではWindows APIを使うのでDeclareも少し増えています。

Option Explicit

' ==================================================
' Declare
' ==================================================
' Rust側の関数定義
#If Win64 Then
    ' Rust API
    Private Declare PtrSafe Function rs_get_message Lib C:\my_rust_lib.dll" Alias "get_message" () As LongPtr
    Private Declare PtrSafe Sub rs_free_string Lib "C:\my_rust_lib.dll" Alias "free_string" (ByVal ptr As LongPtr)

    ' Windows API
    Private Declare PtrSafe Function lstrlen Lib "kernel32" Alias "lstrlenA" (ByVal lpString As LongPtr) As Long
    Private Declare PtrSafe Function lstrlenA Lib "kernel32" (ByVal lpString As LongPtr) As Long
    Private Declare PtrSafe Function MultiByteToWideChar Lib "kernel32" ( _
        ByVal CodePage As Long, _
        ByVal dwFlags As Long, _
        ByVal lpMultiByteStr As LongPtr, _
        ByVal cbMultiByte As Long, _
        ByVal lpWideCharStr As LongPtr, _
        ByVal cchWideChar As Long _
    ) As Long
#End If

' ==================================================
' CONSTANTS
' ==================================================
' UTF-8を示すコードページ番号
Private Const CP_UTF8 As Long = 65001


' ==================================================
' Function
' ==================================================
Sub GetMessageFromRust()
    ' 初期化(念のため)
    Dim ptr As LongPtr
    ptr = 0

    On Error GoTo Finally

    ' ポインタを取得(Rustがメモリ確保)
    ptr = rs_get_message()

    ' nullなら終了
    If ptr = 0 Then GoTo Finally

    ' VBAのStringに変換・コピー
    ' ※ここでメモリの内容がVBAのメモリ領域に複製されます
    Dim strVal As String
    strVal = Utf8PtrToString(ptr)

    MsgBox strVal


Finally:
    If ptr <> 0 Then
        Call rs_free_string(ptr)
        ptr = 0 ' 二重解放防止のためクリアしておく
    End If

    ' エラーが発生していた場合は、エラーを通知して終了
    If Err.Number <> 0 Then
        MsgBox "エラーが発生しました: " & Err.Description, vbCritical
    End If

End Sub


' ==================================================
' Util Function
' ==================================================
' UTF-8ポインタ -> VBA文字列 変換関数
Public Function Utf8PtrToString(ptr As LongPtr) As String
    Dim strlen As Long
    Dim bufSize As Long
    Dim buffer() As Byte

    If ptr = 0 Then Exit Function

    ' 1. UTF-8文字列のバイト長を取得(null文字まで)
    strlen = lstrlenA(ptr)
    If strlen = 0 Then Exit Function

    ' 2. 変換後のUTF-16文字数を計算(まずはバッファサイズを知るため0を渡す)
    bufSize = MultiByteToWideChar(CP_UTF8, 0, ptr, strlen, 0, 0)

    If bufSize > 0 Then
        ' 3. バッファ確保(VBAのStringはUTF-16なので、バッファを埋めればStringとして扱える)
        Utf8PtrToString = String$(bufSize, vbNullChar)

        ' 4. 実際に変換してVBAのString領域に書き込む
        ' StrPtrで文字列変数のメモリアドレスを渡す
        Call MultiByteToWideChar(CP_UTF8, 0, ptr, strlen, StrPtr(Utf8PtrToString), bufSize)
    End If
End Function

VBAではtry{} catch{}構文は存在しませんが、ラベルを設定することでエラー時でも必ず解放されるように設定することができます。
ただし、ブレークポイントなどで止めてそのまま中断しrs_free_stringが実行されない場合はメモリが解放されずそのまま残ることがあるので、注意してください。

成功すると下図のようになります。

Rust側から文字列取得.PNG

上記の方法ではメモリ解放関数を呼び忘れるとメモリリークになるため、取り扱いが非常に繊細です。

実践 - VBAが確保したメモリにRustがデータを書き込む(バッファ渡し)

そこで次はVBA側が確保したメモリにRustが直接データを書き込むという方式を紹介します。
VBA側がメモリの所有権を持っているので、VBA側の参照カウント方式でメモリ解放を行えます。
実務ではこちらの方が使うことが多いと思いますし、こちらを推奨します。何せ解放忘れが起きなくて安全ですので...

Rust側のコードは下記のとおりです。今回は解放用関数は必要ありません。

use std::cmp;
use std::ptr;

#[unsafe(no_mangle)]
pub unsafe extern "system" fn get_greeting_safe(buffer: *mut u16, capacity: i32) -> i32 {
    unsafe {
        // 書き込みたい文字列
        let message = "Hello from Rust";

        // UTF-16にエンコード(VBAはUTF-16のため)
        let utf16_vec: Vec<u16> = message.encode_utf16().collect();
        let msg_len = utf16_vec.len();

        // バッファがNullまたはサイズ0なら、必要なサイズだけ返して終了
        if buffer.is_null() || capacity <= 0 {
            return msg_len as i32;
        }

        // 書き込める最大長を計算(バッファサイズか、メッセージ長の小さい方)
        let copy_len = cmp::min(msg_len, capacity as usize);

        // メモリコピー実行
        ptr::copy_nonoverlapping(utf16_vec.as_ptr(), buffer, copy_len);

        // 実際に書き込んだ文字数を返す
        copy_len as i32
    }
}

VBA側の実装です。

' 戻り値は実際に書き込まれた文字数
Private Declare PtrSafe Function rs_get_greeting_safe Lib "C:\my_rust_lib.dll" Alias "get_greeting_safe" (ByVal buffer As LongPtr, ByVal capacity As Long) As Long

Sub TestSafeString()
    ' バッファの確保(例: 255文字分のスペースを確保し、Null文字で埋める)
    Dim buffer As String
    buffer = String(255, vbNullChar)

    ' Rust関数呼び出し
    ' StrPtrでバッファの先頭アドレスを、Lenで容量を渡す
    Dim writtenLen As Long
    writtenLen = rs_get_greeting_safe(StrPtr(buffer), Len(buffer))

    ' 結果の整形
    If writtenLen > 0 Then
        ' バッファの左側から、書き込まれた分だけ切り出す
        Dim result As String
        result = Left(buffer, writtenLen)

        MsgBox "Result:  " & result & ",  Rust Return: " & writtenLen
    Else
        MsgBox "書き込み失敗、またはバッファ不足"
    End If
End Sub

結果は下記のようになります。

VBA確保メモリにRustが書き込み.PNG

こちらの方法の方がVBA側のコードがすっきりしますし、メモリ解放忘れもないため安全です。
特別な事情がない限りこちらのバッファ渡し方式を使った方がいいと思います。

コードとしてはすごく複雑というわけではないですが、頭の中でいろいろなことを考えながらコードを書く必要があるため、
数値系の受け渡しよりは頭を使います。

今回は数値系と文字列を取り扱いましたが、配列や構造体についても記事を書きたいと思います。
配列の取り扱いを覚えればセルに数万行を一瞬で展開することも可能です。

最近はAIや機械学習などが流行っていますが、このような泥臭いコードを書くのも楽しいので、
気になった方はやってみてはどうでしょうか~😎

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?