2021/12/29 追記
windows-rs 版の記事も書きました。windows-rs を使うときは参考にしてください。
動機
Excel 便利ですよね。うちの会社も例に漏れず Excel であらゆる文書を作成しています。ただ客先とかメーカに文書を配布するときは PDF で送付なんてことがよくあるので、大量の Excel を PDF に変換するということが発生します。お手軽にその作業をやるなら VBScript とか Python を使うのが一般的?と思いますが、あえて Rust でやってみたいと思います。
環境
Windows10 64bit
Excel 2010
rustc 1.46.0
winapi 0.3.9
VBScript
↓こういうこと(Excel を PDF に変換)がしたい。Rust で。
Set oXlsApp = CreateObject("Excel.Application")
If oXlsApp is Nothing Then
WScript.Quit
End If
oXlsApp.Application.Visible = False
oXlsApp.Application.DisplayAlerts = False
Set wb = oXlsApp.Application.Workbooks.Open("path to xls")
Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
wb.Close(False)
oXlsApp.Quit
Set oXlsApp = Nothing
実装
実際には How to automate Excel from C++ without using MFC or #import という Microsoft 公式のドキュメントを愚直に Rust に写経していくだけの簡単な作業です。そんなわけで unsafe なコードをゴリゴリ書いていくので Rust を使う理由はないです。ただこういうコードを書くと参照とポインタの関係とか文字コードのこととか色々な知識が蓄積されていくので、これはこれで良いのではないかなと思います。
Win32 API
Rust で Win32 API 使うとなると winapi crate です。あと Microsoft の公式ドキュメントが欠かせませんね。
Rust から呼びたい Win32 API があったら winapi crate のドキュメントで検索してどのモジュールに含まれているかを確認。Cargo.toml
の features
にモジュール名を追加して、main.rs
にuse winapi::um::objbase::CoInitialize
と書いてインポートしていきます。
[dependencies.winapi]
version = "0.3"
features = ["combaseapi", "objbase"]
use winapi::{
um::{
combaseapi:CoUninitialize,
objbase::CoInitialize
}
};
CoInitialize
まずCoInitialize
関数で COM ライブラリを初期化する必要があるらしいです。お約束みたいなものだと考えておきます。新しいアプリは CoInitializeEx を使えと書いてありますが愚直に写経するので今回無視してCoInitialize
を使います。あとドキュメントに初期化したら必ず CoUninitialize を呼べと書いてあるのでimpl Drop
を使うことにしました。以下、C
とRust
を並べて書いていきます。
// Initialize COM for this thread...
CoInitialize(NULL);
// いろいろな処理
// 途中でリターンして CoUninitialize を呼び忘れることも...
// Uninitialize COM for this thread...
CoUninitialize();
struct Com;
impl Drop for Com {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
unsafe {
// Initialize COM for this thread...
CoInitialize(ptr::null_mut());
let _com = Com;
// いろいろな処理
// スコープを抜けるときに drop が自動的に呼ばれる
// Uninitialize COM for this thread...
}
こうしておけば終了処理を忘れることもないですね!
CLSIDFromProgID
次にCLSIDFromProgID
API で CLSID を取得します。この関数の第1引数はLPCOLESTR
という見たこともない型ですが、UTF-16 です。第2引数はCLSID
構造体へのポインタです。C はCLSID clsid;
という記述で構造体のメモリ領域を確保できますが、Rust ではメンバもすべて書く必要があります。それはさすがに面倒なのでstd::mem::MaybeUninit
で確保します。エラーの場合の処置は anyhow crate のensure!
マクロが便利だと思います。
// Get CLSID for our server...
CLSID clsid;
HRESULT hr = CLSIDFromProgID(L"Excel.Application", &clsid);
if(FAILED(hr)) {
::MessageBox(NULL, "CLSIDFromProgID() failed", "Error", 0x10010);
return -1;
}
use anyhow::ensure;
// Get CLSID for our server...
let mut clsid = init::<CLSID>();
let id = l("Excel.Application");
let hr = CLSIDFromProgID(id.as_ptr(), &mut clsid);
ensure!(SUCCEEDED(hr), "CLSIDFromProgID failed");
fn l(source: &str) -> Vec<u16> {
source.encode_utf16().chain(Some(0)).collect()
}
unsafe fn init<T>() -> T {
mem::MaybeUninit::<T>::uninit().assume_init()
}
CoCreateInstance
続いてCoCreateInstance
でなにやらオブジェクトを作成するようです。Rust に写経するときに困るのが、たまに winapi crate で定義されていない定数があったり、ポインタ周りの扱いです。
IID_IDispatch
は Microsoft の公式ドキュメントによるとこんな値です。winapi crate ではこの値が定義されていないようなので、自分で書きます。
CoCreateInstance
関数の最後の引数はIDispatch
へのポインタを void ポインタにキャストしていますね。Rust の場合は&mut pxlapp as *mut *mut IDispatch as *mut *mut c_void
にように書く必要があります。
// Start server and get IDispatch...
IDispatch *pXlApp;
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_IDispatch, (void **)&pXlApp);
if(FAILED(hr)) {
::MessageBox(NULL, "Excel not registered properly", "Error", 0x10010);
return -2;
}
use winapi::shared::guiddef::GUID;
const IID_IDISPATCH: GUID = GUID {
Data1: 0x00020400,
Data2: 0x0,
Data3: 0x0,
Data4: [0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46],
};
// Start server and get IDispatch...
let mut pxlapp = ptr::null_mut::<IDispatch>();
let hr = CoCreateInstance(
&clsid,
ptr::null_mut(),
CLSCTX_LOCAL_SERVER,
&IID_IDISPATCH,
&mut pxlapp as *mut *mut IDispatch as *mut *mut c_void,
);
ensure!(SUCCEEDED(hr), "Excel not registered properly");
VARIANT
最後にVARIANT
構造体の説明です。Microsoft の公式ドキュメントを見ての通りunion
にたくさんの型がありますね。vt
でデータの型を指定して、実際の値はunion
に保存するようです。Rust の winapi crate ではちょっと冗長な書き方になります。
// Make it visible (i.e. app.visible = 1)
VARIANT x;
x.vt = VT_I4;
x.lVal = 1;
AutoWrap(DISPATCH_PROPERTYPUT, NULL, pXlApp, L"Visible", 1, x);
// Make it visible (i.e. app.visible = 1)
let mut x = init::<VARIANT>();
x.n1.n2_mut().vt = VT_I4 as u16;
*x.n1.n2_mut().n3.lVal_mut() = 0; //Excel のウィンドウを非表示にするなら "0"
auto_wrap(
DISPATCH_PROPERTYPUT,
ptr::null_mut(),
pxlapp,
"Visible",
vec![x].as_mut_slice(),
)?;
まとめ
大事なことはだいたい書いたのでサンプルコードをまとめてどうぞ。
コピペ用サンプルコード
[package]
name = "excel_to_pdf"
version = "0.1.0"
authors = ["benki"]
edition = "2018"
[dependencies]
anyhow = "1.0"
[dependencies.winapi]
version = "0.3"
features = ["combaseapi", "objbase", "winerror", "guiddef", "oaidl", "wtypesbase", "ntdef", "wtypes", "oleauto"]
use anyhow::{anyhow, ensure, Result};
use std::mem;
use std::path::Path;
use std::ptr;
use winapi::{
ctypes::c_void,
shared::{
guiddef::{CLSID, GUID, IID_NULL},
ntdef::{LOCALE_SYSTEM_DEFAULT, LOCALE_USER_DEFAULT},
winerror::SUCCEEDED,
wtypes::{VT_BOOL, VT_BSTR, VT_I4, VT_INT},
wtypesbase::CLSCTX_LOCAL_SERVER,
},
um::{
combaseapi::{CLSIDFromProgID, CoCreateInstance, CoUninitialize},
oaidl::{IDispatch, DISPID, DISPID_PROPERTYPUT, DISPPARAMS, VARIANT},
objbase::CoInitialize,
oleauto::{
SysAllocString, SysFreeString, VariantInit, DISPATCH_METHOD, DISPATCH_PROPERTYGET,
DISPATCH_PROPERTYPUT,
},
},
};
const IID_IDISPATCH: GUID = GUID {
Data1: 0x00020400,
Data2: 0x0,
Data3: 0x0,
Data4: [0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46],
};
const XLTYPEPDF: i32 = 0;
struct Com;
impl Drop for Com {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
fn main() -> Result<()> {
excel_to_pdf("path to xls")?;
Ok(())
}
fn excel_to_pdf<P: AsRef<Path>>(path: P) -> Result<()> {
unsafe {
// Initialize COM for this thread...
CoInitialize(ptr::null_mut());
let _com = Com;
// Get CLSID for our server...
let mut clsid = init::<CLSID>();
let id = l("Excel.Application");
let hr = CLSIDFromProgID(id.as_ptr(), &mut clsid);
ensure!(SUCCEEDED(hr), "CLSIDFromProgID failed");
// Start server and get IDispatch...
let mut pxlapp = ptr::null_mut::<IDispatch>();
let hr = CoCreateInstance(
&clsid,
ptr::null_mut(),
CLSCTX_LOCAL_SERVER,
&IID_IDISPATCH,
&mut pxlapp as *mut *mut IDispatch as *mut *mut c_void,
);
ensure!(SUCCEEDED(hr), "Excel not registered properly");
// Make it visible (i.e. app.visible = 1)
let mut visible = init::<VARIANT>();
visible.n1.n2_mut().vt = VT_I4 as u16;
*visible.n1.n2_mut().n3.lVal_mut() = 0;
auto_wrap(
DISPATCH_PROPERTYPUT,
ptr::null_mut(),
pxlapp,
"Visible",
vec![visible].as_mut_slice(),
)?;
let mut display_alerts = init::<VARIANT>();
display_alerts.n1.n2_mut().vt = VT_I4 as u16;
*display_alerts.n1.n2_mut().n3.lVal_mut() = 0;
auto_wrap(
DISPATCH_PROPERTYPUT,
ptr::null_mut(),
pxlapp,
"DisplayAlerts",
vec![display_alerts].as_mut_slice(),
)?;
// Get Workbooks collection
let mut result = init::<VARIANT>();
VariantInit(&mut result);
auto_wrap(
DISPATCH_PROPERTYGET,
&mut result,
pxlapp,
"Workbooks",
vec![].as_mut_slice(),
)?;
let pxlbooks = *result.n1.n2().n3.pdispVal();
ensure!(!pxlbooks.is_null(), "Workbooks failed");
let path_ = l(path.as_ref().to_string_lossy().as_ref());
let mut param = init::<VARIANT>();
param.n1.n2_mut().vt = VT_BSTR as u16;
*param.n1.n2_mut().n3.bstrVal_mut() = SysAllocString(path_.as_ptr());
// Call Workbooks.Open() to get a new workbook...
let mut result = init::<VARIANT>();
VariantInit(&mut result);
auto_wrap(
DISPATCH_PROPERTYGET,
&mut result,
pxlbooks,
"Open",
vec![param].as_mut_slice(),
)?;
let pxlbook = *result.n1.n2().n3.pdispVal();
SysFreeString(*param.n1.n2().n3.bstrVal());
// Get ActiveSheet object
let mut result = init::<VARIANT>();
VariantInit(&mut result);
auto_wrap(
DISPATCH_PROPERTYGET,
&mut result,
pxlbook,
"ActiveSheet",
vec![].as_mut_slice(),
)?;
let pxlsheet = *result.n1.n2().n3.pdispVal();
ensure!(!pxlsheet.is_null(), "Open workbook failed. {}", path.as_ref().to_string_lossy());
// Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
let mut typ = init::<VARIANT>();
typ.n1.n2_mut().vt = VT_INT as u16;
*typ.n1.n2_mut().n3.intVal_mut() = XLTYPEPDF;
let parent = path.as_ref().parent().ok_or(anyhow!("Cannot get parent directory"))?;
let file_stem = path.as_ref().file_stem().ok_or(anyhow!("Cannot get file_stem"))?.to_string_lossy();
let pdf_file_name = format!("{}.pdf", file_stem);
let pdf_path = parent.join(pdf_file_name);
let pdf_path_ = l(pdf_path.to_string_lossy().as_ref());
let mut file_name = init::<VARIANT>();
file_name.n1.n2_mut().vt = VT_BSTR as u16;
*file_name.n1.n2_mut().n3.bstrVal_mut() = SysAllocString(pdf_path_.as_ptr());
auto_wrap(
DISPATCH_METHOD,
ptr::null_mut(),
pxlsheet,
"ExportAsFixedFormat",
vec![file_name, typ].as_mut_slice(), // 引数の順番が逆になります!
)?;
SysFreeString(*file_name.n1.n2().n3.bstrVal());
// wb.Close(False)
let mut close = init::<VARIANT>();
close.n1.n2_mut().vt = VT_BOOL as u16;
*close.n1.n2_mut().n3.boolVal_mut() = 0;
auto_wrap(
DISPATCH_METHOD,
ptr::null_mut(),
pxlbook,
"Close",
vec![close].as_mut_slice(),
)?;
// Tell Excel to quit (i.e. App.Quit)
auto_wrap(
DISPATCH_METHOD,
ptr::null_mut(),
pxlapp,
"Quit",
vec![].as_mut_slice(),
)?;
// Release references...
(*pxlsheet).Release();
(*pxlbook).Release();
(*pxlbooks).Release();
(*pxlapp).Release();
// Uninitialize COM for this thread...
}
Ok(())
}
// AutoWrap() - Automation helper function...
unsafe fn auto_wrap(
auto_type: u16,
pvresult: *mut VARIANT,
pdisp: *mut IDispatch,
ptname: &str,
args: &mut [VARIANT],
) -> Result<()> {
ensure!(
!(pdisp as *mut IDispatch).is_null(),
"null IDispatch passed to auto_wrap"
);
// Variables used...
let mut dp = DISPPARAMS {
rgvarg: ptr::null_mut(),
rgdispidNamedArgs: ptr::null_mut(),
cArgs: 0,
cNamedArgs: 0,
};
let mut dispid_named = DISPID_PROPERTYPUT;
let mut dispid: DISPID = 0;
let mut ptname_ = l(ptname);
// Get DISPID for name passed...
let hr = (*pdisp).GetIDsOfNames(
&IID_NULL,
&mut ptname_.as_mut_ptr(),
1,
LOCALE_USER_DEFAULT,
&mut dispid,
);
ensure!(SUCCEEDED(hr), "pdisp.GetIDsOfNames({}) failed", ptname);
// Build DISPPARAMS
dp.cArgs = args.len() as u32;
dp.rgvarg = args.as_mut_ptr();
// Handle special-case for property-puts!
if auto_type & DISPATCH_PROPERTYPUT > 0 {
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &mut dispid_named as *mut i32;
}
// Make the call!
let hr = (*pdisp).Invoke(
dispid,
&IID_NULL,
LOCALE_SYSTEM_DEFAULT,
auto_type as u16,
&mut dp,
pvresult,
ptr::null_mut(),
ptr::null_mut(),
);
ensure!(SUCCEEDED(hr), "IDispatch::Invoke({}) failed", ptname);
Ok(())
}
fn l(source: &str) -> Vec<u16> {
source.encode_utf16().chain(Some(0)).collect()
}
unsafe fn init<T>() -> T {
mem::MaybeUninit::<T>::uninit().assume_init()
}
これで Rust から Excel をゴリゴリいじれるようになりました。やったぜ