動機
下記の記事では winapi を使っていました。
最近 Microsoft 製 windows crate もだいぶ熟してきたようなので書き直してみたいと思います。
windows-rs を 0.29.0 → 0.39.0 にアップデートしました。文字列の扱いなど変更されています。(2022/08/12)
環境
Windows 10 64bit
Excel 2016
rustc 1.63.0
windows 0.39.0
windows crate と windows-sys crate の違い
windows-sys crate は Win32API の関数やら構造体をそのままエクスポートしているだけです。なので、関数の引数がワイド文字列の場合は、Rust の文字列をencode-utf16
で UTF-16 に変換する必要があります。また、戻り値がHRESULT
であったり、ポインタで値が返ってきたりするので Rust の世界で扱うにはちょっと面倒です。
一方で windows crate は Win32API を Rust っぽく書けるように、いろんな trait を実装しています。文字列の引数にはそのまま Rust の文字列を渡せますし(0.39.0 では w! マクロを使用する)、戻り値はResult
に包まれて返ってきます!特別な理由がない限りは windows crate を使うべきでしょう。
例えば以下のような感じです。
// Rust の世界の文字列を UTF-16 に変換
let mut id: Vec<u16> = "Excel.Application".encode_utf16().chain(Some(0)).collect();
// 結果を受け取るための変数。mem::zeroed の使用は非推奨
let mut clsid: GUID = std::mem::zeroed();
// 戻り値は HRESULT
let hr = CLSIDFromProgID(id.as_mut_ptr(), &mut clsid);
// エラーチェック
if hr < 0 {
return Err(..);
}
// 0.39.0。Rust の世界の文字列を w! マクロで変換する。
let clsid = CLSIDFromProgID(w!("Excel.Application"))?;
あと、std feature を有効にすることで 0.39.0 では std、alloc feature はなくなっています。windows::core::Error
にstd::error::Error
trait が実装されるので、エラー処理に anyhow が使えるようになります。alloc feature を有効にすることで、impl IntoParam<PWSTR> for &str
が有効になって Rust の文字列をそのまま関数に渡せるようになります。
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.39"
features = [
"Win32_System_Com",
"Win32_Foundation",
]
use anyhow::Result;
use windows::{w, Win32::System::Com::CLSIDFromProgID};
fn main() -> Result<()> {
let _clsid = unsafe { CLSIDFromProgID(w!("Excel.Application"))? };
Ok(())
}
すばらしい
構造体
windows crate では Win32API の構造体に Default trait が実装されています。winapi crate のときはヘルパー関数を使って構造体のメモリ領域を確保していましたが、windows crate なら (構造体)::default();
って書くだけで良いです。いいね!
let variant = init::<VARIANT>();
fn init::<T>() -> T {
std::mem::MaybeUninit::<T>::uninit().assume_init()
}
let variant = VARIANT::default();
2021/12/31 追記。winapi crate でも impl-default feature を有効にするとdefault()
で初期化できるとのことです。@yuma140902 さんに winapi 版記事のコメントで教えてもらいました。
[dependencies.winapi]
version = "0.3"
features = [
"impl-default",
"oaidl"
]
use winapi::um::oaidl::VARIANT;
fn main() {
let _variant = VARIANT::default();
}
VARIANT 構造体について
今回のように COM を扱っているとVARIANT
構造体が出てくるわけですが、windows crate ではVARIANT
構造体のメンバがManuallyDrop
で包まれています。つまりクライアント側でメモリ開放の面倒を見てやる必要があります。(なんでこんな実装になってるのかな?)とりあえずVARIANT
構造体を自分で作ったVariant
構造体に捕捉しておいて、Variant
構造体がdrop
するときにメンバの型によってManuallyDrop
なメンバをdrop
するようにしておきました。こんな実装で問題ない?
struct Variant(VARIANT);
impl Drop for Variant {
fn drop(&mut self) {
unsafe {
match VARENUM(self.0.Anonymous.Anonymous.vt as i32) {
VT_BSTR => ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.bstrVal),
VT_DISPATCH => ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.pdispVal),
_ => (),
}
ManuallyDrop::drop(&mut self.0.Anonymous.Anonymous);
}
}
}
let mut variant = VARIANT::default();
(*variant.Anonymous.Anonymous).vt = VT_BSTR.0 as u16;
(*variant.Anonymous.Anonymous).Anonymous.bstrVal = ManuallyDrop::new("test".into());
// VARIANT を Variant 構造体に捕捉
let variant = Variant(variant);
// Variant が drop するときに ManuallyDrop が drop される
上記 2021/12/31 に訂正しました。当初core::mem::drop
でリソース開放しているつもりでしたが、core::mem::drop
に参照を渡しても意味ないですね…。Manually::drop
を呼ぶべきでした。
まとめ
大事なことはだいたい書いたのでサンプルコードをまとめてどうぞ。
コピペ用サンプルコード
[package]
name = "excel_to_pdf"
version = "0.1.0"
authors = ["benki"]
edition = "2021"
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.39"
features = [
"Win32_System_Com",
"Win32_Foundation",
"Win32_System_Ole"
]
use anyhow::{Context, Result};
use std::mem::ManuallyDrop;
use std::path::Path;
use std::ptr;
use windows::{
core::{GUID, HSTRING, PWSTR},
w,
Win32::System::{
Com::{
CLSIDFromProgID, CoCreateInstance, CoInitialize, CoUninitialize, IDispatch,
CLSCTX_LOCAL_SERVER, DISPPARAMS, VARIANT,
},
Ole::{
DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT, DISPID_PROPERTYPUT,
VARENUM, VT_BOOL, VT_BSTR, VT_DISPATCH, VT_I4,
},
},
};
fn main() {
match excel_to_pdf("path to excel")
{
Ok(_) => (),
Err(e) => println!("{}", e),
}
}
struct Com;
impl Drop for Com {
fn drop(&mut self) {
unsafe {
CoUninitialize();
}
}
}
struct Variant(VARIANT);
impl Variant {
fn dispval(&self) -> Option<&IDispatch> {
unsafe { (*(*self.0.Anonymous.Anonymous).Anonymous.pdispVal).as_ref() }
}
}
impl Drop for Variant {
fn drop(&mut self) {
unsafe {
match VARENUM(self.0.Anonymous.Anonymous.vt as i32) {
VT_BSTR => ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.bstrVal),
VT_DISPATCH => {
ManuallyDrop::drop(&mut (*self.0.Anonymous.Anonymous).Anonymous.pdispVal)
}
_ => (),
}
ManuallyDrop::drop(&mut self.0.Anonymous.Anonymous);
}
}
}
fn excel_to_pdf<P: AsRef<Path>>(path: P) -> Result<()> {
unsafe {
// Initialize COM for this thread...
CoInitialize(ptr::null())?;
let _com = Com;
// Get CLSID for our server...
let clsid = CLSIDFromProgID(w!("Excel.Application"))?;
// Start server and get IDispatch...
let pxlapp: IDispatch = CoCreateInstance(&clsid, None, CLSCTX_LOCAL_SERVER)?;
let pxlapp = Some(&pxlapp);
// Make it visible (i.e. app.visible = 1)
let visible = variant_int(0);
auto_wrap(DISPATCH_PROPERTYPUT, pxlapp, w!("Visible"), vec![visible])?;
let display_alerts = variant_int(0);
auto_wrap(
DISPATCH_PROPERTYPUT,
pxlapp,
w!("DisplayAlerts"),
vec![display_alerts],
)?;
// Get Workbooks collection
let result = auto_wrap(DISPATCH_PROPERTYGET, pxlapp, w!("Workbooks"), vec![])?;
let pxlbooks = result.dispval();
// Call Workbooks.Open() to get a new workbook...
let param = variant_bstr(&path.as_ref().to_string_lossy());
let result = auto_wrap(DISPATCH_PROPERTYGET, pxlbooks, w!("Open"), vec![param])?;
let pxlbook = result.dispval();
// Get ActiveSheet object
let result = auto_wrap(DISPATCH_PROPERTYGET, pxlbook, w!("ActiveSheet"), vec![])?;
let pxlsheet = result.dispval();
// Call wb.ActiveSheet.ExportAsFixedFormat(xlTypePDF, "path to pdf")
let typ = variant_int(0);
let parent = path.as_ref().parent().context("no parent.")?;
let file_stem = path.as_ref().file_stem().context("no file_stem.")?;
let pdf_path = parent.join(format!("{}.pdf", file_stem.to_string_lossy()));
let file_name = variant_bstr(pdf_path.to_string_lossy());
auto_wrap(
DISPATCH_METHOD,
pxlsheet,
w!("ExportAsFixedFormat"),
vec![file_name, typ],
)?;
// wb.Close(False)
let close = variant_bool(false);
auto_wrap(DISPATCH_METHOD, pxlbook, w!("Close"), vec![close])?;
// Tell Excel to quit (i.e. App.Quit)
auto_wrap(DISPATCH_METHOD, pxlapp, w!("Quit"), vec![])?;
// Automatically release references...
// Automatically uninitialize COM for this thread...
}
Ok(())
}
// AutoWrap() - Automation helper function...
unsafe fn auto_wrap(
auto_type: u32,
pdisp: Option<&IDispatch>,
ptname: &HSTRING,
args: Vec<Variant>,
) -> Result<Variant> {
let pdisp = pdisp.context("null IDispatch passed to auto_wrap.")?;
// Variables used...
let mut dp = DISPPARAMS::default();
let mut dispid_named = DISPID_PROPERTYPUT;
let mut dispid = 0;
// Get DISPID for name passed...
pdisp.GetIDsOfNames(
&GUID::default(),
&PWSTR(ptname.as_ptr() as *mut _) as *const _,
1,
0x0400, // LOCALE_USER_DEFAULT, 1024u32
&mut dispid,
)?;
// Build DISPPARAMS
dp.cArgs = args.len() as u32;
dp.rgvarg = args.as_ptr() as *const VARIANT as *mut _;
// Handle special-case for property-puts!
if auto_type & DISPATCH_PROPERTYPUT > 0 {
dp.cNamedArgs = 1;
dp.rgdispidNamedArgs = &mut dispid_named;
}
let mut vresult = VARIANT::default();
// Make the call!
pdisp.Invoke(
dispid,
&GUID::default(),
0x0800, // LOCALE_SYSTEM_DEFAULT, 2048u32
auto_type as u16,
&dp,
&mut vresult,
ptr::null_mut(),
ptr::null_mut(),
)?;
Ok(Variant(vresult))
}
unsafe fn variant_int(value: i32) -> Variant {
let mut variant = VARIANT::default();
(*variant.Anonymous.Anonymous).vt = VT_I4.0 as u16;
(*variant.Anonymous.Anonymous).Anonymous.lVal = value;
Variant(variant)
}
unsafe fn variant_bstr<S: AsRef<str>>(value: S) -> Variant {
let mut variant = VARIANT::default();
(*variant.Anonymous.Anonymous).vt = VT_BSTR.0 as u16;
// impl From<&str> for BSTR が実装されているので into() 呼ぶだけ!
// impl Drop for BSTR も実装されているので、SysFreeString しなくても良い!
(*variant.Anonymous.Anonymous).Anonymous.bstrVal = ManuallyDrop::new(value.as_ref().into());
Variant(variant)
}
unsafe fn variant_bool(value: bool) -> Variant {
let mut variant = VARIANT::default();
(*variant.Anonymous.Anonymous).vt = VT_BOOL.0 as u16;
(*variant.Anonymous.Anonymous).Anonymous.boolVal = if value { 1 } else { 0 };
Variant(variant)
}
winapi crate から windows crate に移行してみました。windows crate の方が Rust っぽく書けるので良いと思います