Quantcast
Channel: いげ太の日記
Viewing all 26 articles
Browse latest View live

[VBA] 長い長い FizzBuzz

$
0
0

VBA でショート コーディングすることは難しい。ならいっそ、ロング コーディングしてみてはどうか。無論、DRY 原則を破らずに。これは VBA のライブラリ不足への挑戦だ。題材は FizzBuzzで。

まず、FizzBuzz のメインの処理と言っていい部分、数値を取って適切な文字列を返す関数を実装する。

' Program.bas

Option Explicit

Function FizzBuzz(ByVal n As Long) As String
SelectCase BitFlag(n Mod 5 = 0, n Mod 3 = 0)
Case 0: FizzBuzz = CStr(n)
Case 1: FizzBuzz = "Fizz"
Case 2: FizzBuzz = "Buzz"
Case 3: FizzBuzz = "FizzBuzz"
CaseElse: Err.Raise 51 ' UNREACHABLE
EndSelect
EndFunction

こうだ。特に難しいところはないが、BitFlag 関数がまだ未定義である。これは何か。文字通りの関数である。Boolean 型の可変長引数を取り、それがたとえば (False) であれば0、(True) であれば 1、(Ture, Flase) であれば2を返す関数であり、すなわち2進数のビット表現を整数値として返すものである。

' Core.bas

Option Explicit

PublicFunction BitFlag(ParamArray flgs() As Variant) As Long
BitFlag = 0
Dim ub As Long: ub = UBound(flgs)

Dim i As Integer
For i = 0 To ub
BitFlag = BitFlag + IIf(flgs(i), 1, 0) * 2 ^ (ub - i)
Next
EndFunction

早くも FizzBuzz の核となるコードは完成だ。

そして、あとはこれをリスト処理するだけである。一つの方向性として、僕は次のように書きたい。

' Program.bas

''' EntryPoint
Sub Main()
Debug.Print Join(ArrMap(Init(New Func, vbString, AddressOf FizzBuzz), ArrRange(1&, 100&)))
EndSub

1から100までの範囲の数値を要素に含む配列を生成し、その配列の各要素を FizzBuzz 関数で加工した配列を新たに生成し、それを Join して表示する、という流れだ。ここで未定義なものは、ArrRange 関数、ArrMap 関数、Func クラス、Init 関数である。以下にこれらの実装をそれぞれ示そう。

ArrRange はある値から値までの範囲の配列を生成する。さして難しいコードではないが、配列長が不定のものをいかにうまく伸長させるかはちょっとしたトピックだ。

' Core.bas

PublicFunction IncrPst(ByRef n As Variant, OptionalByVal stepVal As Variant = 1) As Variant
IncrPst = n: n = n + stepVal
EndFunction

PublicFunction ArrRange( _
ByVal fromVal As Variant, ByVal toVal As Variant, OptionalByVal stepVal As Variant = 1 _
) As Variant

'FIXME: parameters type check

Dim i As Long: i = 0
Dim alen As Long: alen = 32
Dim arr As Variant: ReDim arr(alen - 1)

SelectCase stepVal
CaseIs> 0
DoWhile fromVal <= toVal
arr(IncrPst(i)) = IncrPst(fromVal, stepVal)
If i >= alen Then alen = alen * 2: ReDimPreserve arr(alen - 1)
Loop
CaseIs< 0
DoWhile fromVal >= toVal
arr(IncrPst(i)) = IncrPst(fromVal, stepVal)
If i >= alen Then alen = alen * 2: ReDimPreserve arr(alen - 1)
Loop
CaseElse
Err.Raise 5
EndSelect

If i > 0 Then
ReDimPreserve arr(i - 1)
Else
arr = Array()
EndIf
ArrRange = arr
EndFunction

あわせて定義した IncrPst は、C 系言語にはよくある後置インクリメントだ。VBA では i++ のようなことができず、そのためにコードがシンプルになり切れないことがあり、このような代用品を定義した。

続いていこう。ArrMap は、配列の各要素に関数を適用して得た値で配列を生成するために、関数ポインタ(コールバック関数)を引数に取る必要がある、いわば高階関数である。VBA に関数を受け渡しする標準の方法はないから、Win32 API の DispCallFunc の力を借りて関数オブジェクトを実装し、これに対応する。それが Func クラスである。

64-bit 環境にも配慮しながら Func クラスを定義する。

' Func.cls

Option Explicit

#If VBA7 And Win64 Then
PrivateDeclare PtrSafe _
Function DispCallFunc Lib"OleAut32.dll" ( _
ByVal pvInstance As LongPtr, _
ByVal oVft As LongPtr, _
ByVal cc_ As Long, _
ByVal vtReturn As Integer, _
ByVal cActuals As Long, _
ByRef prgvt As Integer, _
ByRef prgpvarg As LongPtr, _
ByRef pvargResult As Variant _
) As Long
#Else
PrivateDeclare _
Function DispCallFunc Lib"OleAut32.dll" ( _
ByVal pvInstance As Long, _
ByVal oVft As Long, _
ByVal cc_ As Long, _
ByVal vtReturn As Integer, _
ByVal cActuals As Long, _
ByRef prgvt As Integer, _
ByRef prgpvarg As Long, _
ByRef pvargResult As Variant _
) As Long
#End If

PrivateEnum tagCALLCONV
CC_FASTCALL = 0
CC_CDECL = 1
CC_MSCPASCAL = CC_CDECL + 1
CC_PASCAL = CC_MSCPASCAL
CC_MACPASCAL = CC_PASCAL + 1
CC_STDCALL = CC_MACPASCAL + 1
CC_FPFASTCALL = CC_STDCALL + 1
CC_SYSCALL = CC_FPFASTCALL + 1
CC_MPWCDECL = CC_SYSCALL + 1
CC_MPWPASCAL = CC_MPWCDECL + 1
CC_MAX = CC_MPWPASCAL
EndEnum

PrivateConst S_OK = &H0

Private xxReturnType As VbVarType
#If VBA7 And Win64 Then
Private xxAddr As LongPtr
#Else
Private xxAddr As Long
#End If

Private xxParamTypeBinding As Boolean
Private xxUbParam As Long
Private xxParamTypes() As Integer

PrivateSub EarlyBindParamTypes(ByVal paramTypes As Variant)
' If Not IsArray(paramTypes) Then Err.Raise 5
xxParamTypeBinding = False

xxUbParam = UBound(paramTypes)

If xxUbParam >= 0 Then
Dim i As Long
ReDim xxParamTypes(xxUbParam)
For i = 0 To xxUbParam: xxParamTypes(i) = paramTypes(i): Next

xxParamTypeBinding = True
EndIf
EndSub

PrivateSub LateBindParamTypes(ByVal params As Variant)
' If Not IsArray(params) Then Err.Raise 5

xxUbParam = UBound(params)

If xxUbParam >= 0 Then
Dim i As Long
ReDim xxParamTypes(xxUbParam)
For i = 0 To xxUbParam: xxParamTypes(i) = VarType(params(i)): Next
Else
ReDim xxParamTypes(0)
EndIf

xxParamTypeBinding = True
EndSub

#If VBA7 And Win64 Then
PublicSub Init( _
ByVal retrunType As VbVarType, ByVal addr As LongPtr, ParamArray paramTypes() As Variant _
)
#Else
PublicSub Init( _
ByVal retrunType As VbVarType, ByVal addr As Long, ParamArray paramTypes() As Variant _
)
#End If

xxReturnType = retrunType
xxAddr = addr
EarlyBindParamTypes paramTypes
EndSub

PublicSub CallByPtr(ByRef returnValue As Variant, ByVal params As Variant)
' If Not IsArray(params) Then Err.Raise 5

#If VBA7 And Win64 Then
Dim lpParams() As LongPtr
#Else
Dim lpParams() As Long
#End If
If xxUbParam >= 0 Then
Dim i As Long
ReDim lpParams(xxUbParam)
For i = 0 To xxUbParam: lpParams(i) = VarPtr(params(i)): Next
Else
ReDim lpParams(0)
EndIf

Dim stat As Long, ret As Variant
stat = DispCallFunc( _
0, xxAddr, tagCALLCONV.CC_STDCALL, xxReturnType, _
xxUbParam + 1, xxParamTypes(0), lpParams(0), ret)
If stat <> S_OK Then Err.Raise 5

If IsObject(ret) ThenSet returnValue = ret ElseLet returnValue = ret
EndSub

PublicSub FastApply(ByRef returnValue As Variant, ParamArray params() As Variant)
IfNot xxParamTypeBinding Then
LateBindParamTypes params
EndIf
CallByPtr returnValue, params
EndSub

PublicFunction Apply(ParamArray params() As Variant) As Variant
IfNot xxParamTypeBinding Then
LateBindParamTypes params
Else
If xxUbParam <> UBound(params) Then Err.Raise 5
EndIf
CallByPtr Apply, params
EndFunction

いい加減ちょっと長いが、これで関数オブジェクトが手に入るのなら安いものだ。これの使い方をすこし見ておこう。

Dim f As Func: Set f = New Func

f.Init vbString, AddressOf FizzBuzz
' f.Init vbString, AddressOf FizzBuzz, vbLong

Debug.Print f.Apply(15) 'FizzBuzz
' Dim ans As String
' f.FastApply(ans, 15)
' Debug.Print ans 'FizzBuzz

Func.Init は初期化のための関数で、第1引数にコールバック関数の戻り値の型(Sub の場合は vbEmpty)、第2引数にコールバック関数のアドレス、第3引数は可変長引数でコールバック関数の仮引数の型を渡す。第3引数は、あとで渡す実引数の型が仮引数の型と一致するなら、省略もできる。

そして Func.Apply を経由してコールバック関数に引数を渡して実行する。通常は Apply を使うが、ほんのちょっとだけ高速かもしれない版として FastApply を使うこともできる。

ところで Func.Init なんてものがなぜ必要かといえば、VBA のクラスには引数をとるコンストラクタが定義できないことによる。クラスのインスタンス化時に情報を渡すことができないとはなんと煩わしいことか。

この煩わしさを、すこしばかり軽減するための関数が Init だ。

' Core.bas

PublicFunction Init(ByVal obj As Object, ParamArray args() As Variant) As Object
SelectCaseUBound(args)
Case -1: obj.Init
Case 0: obj.Init args(0)
Case 1: obj.Init args(0), args(1)
Case 2: obj.Init args(0), args(1), args(2)
Case 3: obj.Init args(0), args(1), args(2), args(3)
Case 4: obj.Init args(0), args(1), args(2), args(3), args(4)
Case 5: obj.Init args(0), args(1), args(2), args(3), args(4), args(5)
Case 6: obj.Init args(0), args(1), args(2), args(3), args(4), args(5), args(6)
Case 7: obj.Init args(0), args(1), args(2), args(3), args(4), args(5), args(6), args(7)
CaseElse: Err.Raise 5
EndSelect
Set Init = obj
EndFunction

紛れもなく純粋に泥臭いコードである。しかしながら、これのおかげでクラスのインスタンス化の記述が楽になる。

Dim f As Func: Set f = Init(New Func, vbString, AddressOf FizzBuzz)

わずか一行の削減と侮ることなかれ。クラスのインスタンス化はコードのいたるところに現れる。それがシンプルに表現できるようになるのには、思う以上のメリットがある。そしてそれは特に、書き手よりも、読み手に対して。

さて大詰。いよいよ ArrMap だ。

' Core.bas

PublicFunction ArrMap(ByVal f As Func, ByVal arr As Variant) As Variant
IfNot IsArray(arr) Then Err.Raise 13
Dim lb As Long: lb = LBound(arr)
Dim ub As Long: ub = UBound(arr)
Dim ret As Variant
If ub - lb < 0 Then
ret = Array()
GoTo Ending
EndIf

ReDim ret(lb To ub)

Dim i As Long
For i = lb To ub: f.FastApply ret(i), arr(i): Next

Ending:
ArrMap = ret
EndFunction

Func の使い方がわかってしまえばどうということはない。引数の配列から返り値の配列へ、コールバックを適用しながらループを回すだけのコードだ。

長くなってしまったから、VBA による FizzBuzz を再度示そう。BitFlag 関数、IncrPst 関数、Init 関数、Func クラス、ArrRange 関数、ArrMap 関数をライブラリとみなした上で、だ。

' Program.bas

Option Explicit

Function FizzBuzz(ByVal n As Long) As String
SelectCase BitFlag(n Mod 5 = 0, n Mod 3 = 0)
Case 0: FizzBuzz = CStr(n)
Case 1: FizzBuzz = "Fizz"
Case 2: FizzBuzz = "Buzz"
Case 3: FizzBuzz = "FizzBuzz"
CaseElse: Err.Raise 51 'UNREACHABLE
EndSelect
EndFunction

''' EntryPoint
Sub Main()
Debug.Print Join(ArrMap(Init(New Func, vbString, AddressOf FizzBuzz), ArrRange(1&, 100&)))
EndSub

長い長い FizzBuzz も、ライブラリが充実していればこの程度だ。

VBA は貧弱だ。しかし本当に貧弱なのは VBA のライブラリだ。VBA の言語機能を嘆く前に、我々はライブラリの充実と、公開と、共有をすべきなのだ。


Micro ORM を128行で

$
0
0

今年も F# Advent Calendar 2013の季節がやってまいりました。というわけで、初日、12/1担当の私です。

タイトルの通りなわけですが。ここで訂正です。「128行で」と申し上げましたが、よく数えたら124行でした。加えて、「Micro ORM」と書きましたが、Micro RRM でした。Micro Record-Relational Mapper です。はいそこ、レコードも結局オブジェクトでしょ? とか言わない。

ソースは github に上げておきましたので、そちらでご覧ください。

https://github.com/igeta/FsugJp.MicroRrm

実装コードはすべて FsugJp.MicroRrm.fsに書いていて、繰り返すようですがたった124行です。で、簡単な使い方を Sample.fsxで示しています。

何ができるか? 何ができないか?

現状、DB からのデータの読み込みのみできます。つまり、SELECT 文を投げて、その結果セットのデータを指定したレコードに詰めて返してくれる、ということだけをします。結果セットとレコードは、対応するように、フィールド名およびその型を合わせておく必要があります。追加・更新・削除はできません。

使い方は?

Sample.fsx の通りですが、ざっと解説。

まず、必要な結果セットに対応するレコードを作ります。いわゆる POCO ですね。POCR と言ってもいいかもしれない、ダメかもしれない。まあ何でもいいです。ありがちには、テーブルに対応するような型として作ります。説明上、たとえば MyTable とでもしましょうか。

んで、それに DB からデータを取得して詰め込むわけですが、そのための API として関数が2個あるだけでして、readBy と read です。名前でお気づきかもですが、実質1個って言ってもいいようなものですね。

readBy から説明します。DbRecord.readBy<MyTable> dbConnection "SELECT * FROM MyTable"のような形で使います。第1引数で指定したコネクションに対して、第2引数で指定した SQL 文字列を実行し、その結果セットを seq<MyTable> として返します。

read は、readBy の SQL 文指定を省略した版です。C# ならオーバー ロードで実装するところですね。DbRecord.read<MyTable> dbConnection のような形で使い、MyTable テーブルのすべてのレコードを取得します。内部的に readBy を呼んでいて、それに渡す "SELECT * FROM MyTable"な SQL を勝手に作ってくれるってだけのものです。

何がうれしいか? 見どころは?

F# 的にうれしいのは、すでに説明した通り、オブジェクトでなくレコードでデータが取得できるというところです。パターン マッチが使えます、比較演算も構造的に行われます、と。それから、Null 許容なフィールドのデータは、Nullable<'T>でなく 'a option の値としてマップするようになってます。やっぱ option でしょ、そうでしょ? すべては構造的な値として、ね。

SQL Server 的にうれしいのは、レコードを DbSchemaAttribute 属性が付与されたモジュール下に定義することによって、そのモジュール名をスキーマ名として使用する、というところ。具体的には、[&ltDbSchema>] module MySchema に MyTable レコードを定義することにより、read によって自動生成される SQL が、"SELECT * FROM MySchema.MyTable"となります。そうでない場合は "SELECT * FROM MyTable"です。まあ、やや蛇足的な機能ではあります。

それと、当たり前に欲しい機能として、レコードのフィールドと結果セットのフィールド、これらが異なる定義である場合でも、きちんと動きます。わかりにくいので具体例で言うと、MyTable レコードにはフィールド F1、F2、F3 を定義しました、でも DB 側の MyTable テーブルにはフィールド F1、F3、F5 がありますよ、という場合でも、ちゃんと F1 と F3 の値だけを取ってきてくれて、F2 にはそのフィールドの型の規定値をセットします。

今後の展開は?

もしご好評いただくようなら、開発を継続、機能拡張していきたいです。ホントに好評なら NuGet に登録したりとか? わかりませんが。

目下、個人的にちょっとやりたいなーと思っているのは、「DB 側のテーブル名やフィールド名はスネーク ケースで作ってるんだけど、F# でマップするときはキャメル ケースにしたいんだよねー」な要求への対応。あと、DB 側の名前に F# のキーワードが使われていた場合への対応とか。

それから追加・更新・削除をどうするか。そもそも ORM にも Micro ORM に詳しくないのでどうすればいいやらという感じですが。

まとめ

F# 的な Micro ORM とは、という議題についての取っ掛かりのようなものになればということで、ごく簡単なコードを示したので作ってみた、というところです。実用性は不明です。

ただ、DB からサクッとデータが取ってこれて、それを fsi でごにょれるの、割と便利。


つーわけで、F# Advent Calendar 2013、初日終了っす。2日目は 7shi さんです。バトンターッチ!

ぼくらが VBA を書く理由

$
0
0

誰かが言った。VBA のここがダメだと。その dis は妥当なものであり、僕も頷けるところだった。

自分で言うのもなんだが、僕は VBA についてよく知っている、まあそれなりには。先の dis についても、僕はその解決のための API を知っていて、それらをどう組み合わせれば便利に使えるかについてのアイデアさえあった。ならば、それを作って示すというのがプログラマーの本分であろう。

あとはこの面倒くさいと思う気持ち、VBA なんかのために貴重なプライベートの時間まで割きたくないと判断する僕の真っ当な脳内スケジューラ実装においてどんな理由を与えて当該タスクのスタベーションを回避するかという問題であった。VBA は残念だ。どこがって、なんかもう全体的に。でもこのアイデアはその残念さを払拭とまではいかないが、いくらか緩和させる可能性を持っている。

考え方を変える必要がある。本当に残念なのは、「VBA なんか」という僕の考え方だ。「VBA なんか僕のキャリアに何のプラスにもならない」という考え、世間擦れだ。残念だと評するのは人である。VBA を残念たらしめているのは世間の空気であり、みんなそう言ってるからそうなんだろうということだ。世間という名もなき集団に評価を丸投げしてそれを鵜呑みにするなんて相当イカれてる。

僕が今変わることでその空気だって変えられるかもしれない。イカれてんのはお前の方だろって? おーけーおーけー。それなら決着をつけようじゃないか。いまから僕がそいつを作るから、いいか、それ見てお前が「すごい」って言ったら僕の勝ちだぜ?

尻に火を付けるための導火線の先を人は魂と呼ぶ。新しいプロジェクトを始めるにはすこし大げさなぐらいがちょうどいい。そうやって僕はこの馬鹿げた挑戦を開始した。

なんて半分ホントで半分ウソの空想の序章。ただでも、VBA の持つ古臭い文化、掲示板文化ホームページ文化はそろそろいい加減遅まきながらも変化を必要としている。

今ぼくらに必要なのはコピー&ペーストのためのサンプルコードではない。よく設計された抽象化基盤とありがちな関数群を提供するオープンなライブラリだ。そしてそれを可能にするために、ソースコードをオープンな場で共有するために、docm や xlsm や accdb といった Office ファイルから、VBA ソースコードをテキストとして抜き出す(またはその逆を行う)ツールが必要だ。

Ariawase is free library for VBA cowboys.

そうさ。ぼくら VBAer だって、カッコいいことしたいじゃん。github で pullreq でってさ。あきらめのその先へ、進もう

Ariawase v0.6.0 解説(vbac 編)

$
0
0

まずはこいつから。ライブラリ本体には興味ないけどこいつは要る、という話もあるとかないとか。

Ariawase is free library for VBA cowboys.

vbac は、フリーの VBA ライブラリ Ariawase に同梱されるツールで、各種のマクロ有効 Office ファイルから VBA コードをエクスポートしたり、あるいは逆にインポートしたりするためのものです。VBA のソースコードを git などでバージョン管理するために、また Emacs や Vim や Sublime Text あるいはサクラエディタなどの任意のエディタで VBA を編集するために、さらに grep や diff のような外部コマンドとの連携を図るために使用できます。

本ツールは、Word ファイル(doc、dot、docm、dotm)、Excel ファイル(xls、xla、xlt、xlsm、xlam、xltm)、Access ファイル(mdb、accdb)に対応しています。Outlook(otm)に関してはその仕様によりサポートができません。PowerPoint については、現状で対応していませんが、単に僕にとって必要ないというだけなので、もしご要望があれば実装します。あと Access adp も今のところ未対応です。

vbac を使用するにあたり、最初にやっておかなければならない設定があります。vbac は VBA プロジェクトにアクセスするので、Word、Excel では、セキュリティ設定を変更する必要があります。Office のバージョンによって設定場所が違いますが、2013 を例に取れば、[オプション]-[セキュリティ センター]-[セキュリティ センターの設定...]-[マクロの設定]から、[VBA プロジェクト オブジェクト モデルへのアクセスを信頼する]のチェックをオンにします。

使い方です。vbac は、vbac.wsf という単一の WSH スクリプト(実装言語は JScript)です。コマンド プロンプトから以下のような形式で実行します。

cscript //nologo vbac.wsf ...

help コマンドを実行すると、あんちょこ程度の簡単なヘルプメッセージを表示します。

>cscript //nologo vbac.wsf help
vbac (version 0.6.0)

Usage: cscript vbac.wsf <command> [<options>]

Commands:
combine Import all VBComponents
decombine Export all VBComponents
clear Remove all VBComponents
help Display this help message

Options:
/binary:<dir> Specify directory of macro-enabled Office files
(default: bin)
/source:<dir> Specify directory of source code files
(default: src)
/vbaproj Use .vbaproj file
/dbcompact With Access DB compaction

それでは実例として、Book1.xlsm から VBA コードをエクスポートしてみます。基本的には、フォルダ構成を以下のようにします。任意のフォルダ(ここでは Book1 とします)に vbac.wsf を置いて、同階層に bin フォルダを作成して対象となる Office ファイル(Book1.xlsm)を格納する、という形です。

Book1
│ vbac.wsf

└─bin
Book1.xlsm

エクスポートには decombine コマンドを使用します。Book1.xlsm には、Class1 クラス モジュール、Module1 標準モジュール、Sheet1 シート モジュールが含まれるものとすると、以下のようになります。

>cscript //nologo vbac.wsf decombine
begin decombine

> Target: Book1.xlsm
- Export: Class1.cls
- Export: Module1.bas
- Export: Sheet1.cls

end

decombine は、src フォルダを作ってそこに対象の Office ファイルと同名となる Book1.xlsm フォルダを作成し、その中に VBA ソース ファイルをエクスポートします。

Book1
│ vbac.wsf

├─bin
│ Book1.xlsm
└─src
└─Book1.xlsm
Class1.cls
Module1.bas
Sheet1.cls

では逆に、src\Book1.xlsm フォルダ内の VBA ソース ファイルを bin\Book1.xlsm にインポートしてみます。これには combine コマンドを使用します。

>cscript //nologo vbac.wsf combine
begin combine

> Target: Book1.xlsm
- Import: Class1.cls
- Import: Module1.bas
- Import: Sheet1.cls

end

combine は、bin\Book1.xlsm ファイルの内容を上書きします。あるいは、bin に Book1.xlsm ファイルが存在しなかった場合には、新しいファイルを作ってインポートを行います。

ここで重大な注意点があります。このインポート処理は、bin\Book1.xlsm ファイルに存在する VBA コードがいったんすべて削除された後で行われます。つまり、src 側にはないモジュールが bin 下の Office ファイルに存在したとすると、それらは消されてなくなってしまう、ということです。

この注意点は、combine だけでなく decombine にも当てはまります。エクスポート処理においても同様に、その前処理として、src\Book1.xlsm フォルダに存在する VBA ソース ファイルがすべて削除されます。これは、VBA ソース ファイルに対してだけ行われるのであり、何か他の、たとえば txt ファイルがあったとしてもそれは単純に無視されます。

つまり combine と decombine は、単にインポート・エクスポートを行うのでなく、いわば同期(ファイル同期)を行うためのコマンドであるということです。

これを踏まえて、X.xlsm と Y.xlsm の2つのファイルから VBA コードをエクスポートして、それらをまとめて新しいファイル Z.xlsm にインポートする、コード統合作業について考えてみます。vbac は複数の Office ファイルに対応します。ですから、まず X.xlsm と Y.xlsm の両方を bin フォルダに格納して decombine します。すると、src\X.xlsm および src\Y.xlsm フォルダに VBA ソース ファイルが作成されます。両フォルダに作成されたファイルを、新しく src\Z.xlsm フォルダを作ってそこにコピーし、今度は combine します。そうすると、2ファイルの VBA コードを統合した bin\Z.xlsm ファイルを得ることができます。

さて、ここまでを箇条書きでまとめます。

  • vbac.wsf と同じ階層に bin フォルダと src フォルダを持つ
  • bin フォルダには Office ファイルを格納する
  • src フォルダには VBA ソース ファイルを格納する
  • エクスポートは decombine コマンドを使用する
  • インポートは combine コマンドを使用する
  • combine および decombine は VBA コードを同期する
  • vbac は複数の Office ファイルにも対応

あとはだいたいおまけ。bin とか src って名前じゃなく違うフォルダを指定したい場合には、/binary および /source オプションを使用します。/binary:Debug\bin のように、コロン区切りでフォルダ パスを指定します。フォルダ パスには絶対パスおよび相対パスが指定でき、相対パスは vbac.wsf が置かれているフォルダを起点とします。

cscript //nologo vbac.wsf combine /binary:Debug\bin /source:code

対象が Access ファイルである場合、/dbcompact オプションを使用すれば、事後処理として[データベースの最適化/修復]をあわせて行うことができます。

cscript //nologo vbac.wsf combine /dbcompact

/vbaproj はちょっとしたオプションです。これを付加して decombine すると、src\Book1.xlsm フォルダに App.vbaproj というファイルが作成されます。これはいわゆるプロジェクト ファイルです。中身は素朴な ini ファイル形式になっていて、プロジェクト プロパティおよび参照設定の情報が出力されます(ただし条件付きコンパイル引数を除く。API がない)。

cscript //nologo vbac.wsf decombine /vbaproj

そして、combine 時に /vbaproj を指定すれば、App.vbaproj に出力されたプロジェクト プロパティと参照設定が bin\Book1.xlsm に復元されます。なお、参照設定の復元に関しても、既存の参照がすべて削除された上で行われますので注意してください。

あまり使う機会はないかもしれませんが、clear コマンドについても紹介しておきましょう。このコマンドは、bin\Book1.xlsm ファイルが含む VBA コードをすべて削除します。

>cscript //nologo vbac.wsf clear
begin clear

> Target: Ariawase.xlsm
- Remove: Class1
- Remove: Module1
- Remove: Sheet1

end

最後に、vbac を使用して git など VCS で VBA を管理する上での Tips です。ここまでの内容で気づかれた方もいるかもしれませんが、vbac.wsf は、案件ごとのフォルダに1つ置いて使用するよう設計されています。リポジトリには vbac.wsf を含めてコミットするのがよいでしょう。

また、特に Excel でその傾向が強いように思いますが、VBA は Office ドキュメント(ワークシート)の内容と一体となって動くものが多いです。そのため、src フォルダ内の VBA ソース ファイルだけでなく bin フォルダに格納した Office ファイルも一緒に、リポジトリにコミットしておいた方がよいかもしれません。

さて、つらつらと解説して参りましたが、vbac は我ながら本当に便利なツールに仕上がっていると思います。VBAer 必携のツールです。ぜひ使ってみてください。

余談。

実は vbac には隠し機能があります。疑り深いあなたのために、combine 時に bin の内容をバックアップするための /binback オプションと、Access ファイルのクエリも管理対象にしたい人のための /incquery オプションです。これらオプションは、実装はされているものの無効化されています。vbac.wsf のソースのずっと下の方で無効化処置が行われていますので、これらを使いたい方は、対応する無効化箇所をコメントアウトして有効化してください。

    // It's guard for internal impl. If necessary, you can comment out to enable this feature.
param.binbak.flag = false;
param.incQuery = false;

/binbak オプションに関して、バックアップは vbac.wsf と同階層の bak フォルダに作成されます。別の場所にバックアップしたい場合は /binbak:tmp などと指定することもできます。

Ariawase v0.6.0 解説(ライブラリ概要編)

$
0
0

Ariawase はフリーな VBA ライブラリです。貧弱な VBA をもうすこしまともに扱うために開発されました。足りない組み込みライブラリ、標準的な方法が存在しない抽象化機構、そして、やりづらいテストを補うべく機能を提供します。

Ariawase is free library for VBA cowboys.

ライブラリの全体像をまずは一覧表で。計11個のモジュールを、ライブラリの基盤となるものから順に4つの区分、Standard、Advance、Option、Other に分けて以下に示します。

  • Standard
    • Core.bas - 基本モジュール
    • IO.bas - 入出力
  • Advance
    • Ext.bas - 拡張モジュール
    • Assert.bas - ユニット テスト
    • ArrayEx.cls - 自動拡張配列
    • Tuple.cls - タプル
    • Func.cls - 関数ポインタ
  • Option
    • WinInet.bas - ファイル ダウンロード
    • CdoMail.cls - メール送信
    • Resource.bas - リソース
  • Other
    • MonkeyTest.cls - Ariawase 自身のテスト

Ariawase は、すべてのモジュールをいつも使用する必要はありません。自分に必要なモジュールだけを取り出して利用するのでも構いません。その場合は、この区分に沿って取捨選択するとよいでしょう。

最小構成は Standard で、これは常に必要となります。より多くの機能を利用する場合は Standard に加えて Advance を使用します。Option の各モジュールは、それぞれ特定の機能を提供するものですので、必要なものがあれば適宜利用します。Other は、あなたのコードのためには必要とはならないでしょう。Ariawase の操作と動作を確認するために使用できますが、あなたのマクロをリリースするときには、最終的に消されるべきものです。

Standard

Ariawase のもっとも基盤となるモジュール群です。

特に Core.bas は必須のモジュールとして設計されており、多くの基本的な関数や、抽象機能を提供します。Core.bas は、他のモジュールへの依存を持たないモジュールとして設計されており、今後においてもこれは変わることのない仕様です。IO.bas はファイルの入出力機能を提供します。ファイルの入出力が必要なければ削除してもかまいませんが、VBA を書くにあたって Ariawase のようなライブラリを必要とする多くの場合、IO.bas は必要となるでしょう。

Advance

より高度な追加機能を提供します。もっとコード寄りの視点で言えば、Core.bas 以外のモジュールにも依存があるような標準モジュールと、その実現のために必要なクラス モジュールがこれに区分されます。

Ext.bas は、Core.bas よりも高度な機能を提供します。現在は、主に配列操作のための関数群を提供しており、いわゆる関数型言語に見るようなリスト処理(配列処理)を実現します。Assert.bas はユニット テストのためのモジュールです。簡単ながらも必要十分なテスト機能を実装します。

ArrayEx.cls は自動拡張する配列です。Tuple.cls はタプルと呼ばれる、不変な値の組を提供するものです。Func.cls は関数ポインタを扱います。これらクラス モジュールは Ext.bas や Assert.bas から使用されますが、もちろんあなたのマクロのために使用することもできます。

Option

特定機能のためのモジュール群です。その機能の必要に応じて利用します。

WinInet.bas は、インターネット上からファイルをダウンロードする機能を持ちます。CdoMail.cls は、電子メールを送信するだけの機能を持ちます。Resource.bas は、CdoMail.cls で使用する定数値を定義します。

これらモジュールは、Ariawase の将来のバージョンからは削除され、別途提供となるかもしれません。

Other

あなたのマクロのリリースのためには不必要なモジュール群です。

MonkeyTest.cls は、Ariawase 自体のテスト コードです。Ariawase のテストを確認したり、各関数の使用例の確認のために利用できますが、あなたのマクロのリリース時には削除されるべきものです。

バージョンについて

というか今後の展望についてですが。

v0.6.0 というバージョン番号からもお分かりの通り、Ariawase はまだベータ的な位置付けです。と言っても、バグがバンバン残っているというわけではありませんが、まだまだ洗練の余地が、API の破壊的変更の可能性がいくらか残されている状況とお考えください。

v1.0 が出せるまではがんばりたい気持ちもあるのですが、一方で、vbac は別として、Ariawase 本体を活用しているという話はあまり聞かないところでありまして、利用されていないものをメンテし続けるほどのモチベーションはないので、今後 Ariawase が活発に更新されるかどうかは普及する・しないによる、といったところです。

ただまあ普及って言っても僕の閾値は低くて、10人ぐらいの人から「使ってるよ!」って言われたら、その気になって開発し続けるとは思いますけど。そうでない場合も、細々とはメンテし続けようとは思っていますし、特にバグ修正に関しては、見つけ次第、ご報告いただき次第対処していきます。

という感じで。

[C#] COM と RCW と参照カウント

$
0
0

NOTE: この記事は、当初、ココログの「いげ太のブログ」で公開していたものです。

いたるところで書かれてはいるものの、個人的に、それその記事ひとつで理解できるというようなズバピタな記事がなかったので学習メモ。ていうか、調べてたらキリがなくなってきたので、いったんフィックスしたい。

ランタイム上で暗躍する RCW

.NET から COM オブジェクトを扱うということは、マネージドからアンマネージにアクセスするということであり、すなわちマーシャリングが必要となる。これは、ランタイム呼び出し可能ラッパー(RCW: Runtime Callable Wrapper)なるしくみによって行われる。

図: クライアント --> RCW --> COM オブジェクト

通常、.NET クライアント(アプリケーション)が直接的に扱うのは、生身の COM オブジェクトではなく、そのラッパーたる RCW である。ランタイム(.NET Framework)によってサポートされるため意識しづらいが、RCW というプロキシを挟んで COM オブジェクトにアクセスしているのだ。これは重要な事実であり、認識しておかなければならないことだ。

COM オブジェクトとメモリ

さて、ここで、COM プログラミングの世界をすこしだけのぞいてみる。COM の規定によれば、COM オブジェクトたるもの「自分のメモリは自分で解放する責任がある」とのこと。そして、それを実現するためには、参照カウントというテクニックが用いられる。

「参照カウント」とはすこしあいまいなネーミングだ。どうせなら「被参照数」と呼びたいところ。要するに、参照カウントとは、その COM オブジェクトが他からどれだけ参照されているかを示す数値である。COM オブジェクトは、自分が使ったメモリを自分で解放するために、自身への参照数を自分で管理していて、参照数が "0"になったら他からの参照がなくなった(=用済み)と判断して、自分自身のメモリを解放するのである。

COM オブジェクトは健気な奴で、いらなくなったら、自らの居場所を明け渡すわけだけれど、だけどもそれ故に、いま誰が必要とし、必要としなくなったのかは、使う側から通達しなくてはならない。因果なものだが、ロジカルな帰結だ。いつまでもメモリに居座らせず、任務を終えた COM オブジェクトは解放してやらないといけない。

GC と参照カウント

.NET の世界に戻ろうか。.NET でメモリ管理といえばガベージ コレクション(GC)である。COM オブジェクトを内包する RCW は、マネージドであり、もちろん GC の管理対象だ。しかし、COM オブジェクトは、自分のメモリは自分で解放する責務を負っている。

ここに、メモリ管理のアプローチの違いからくるミスマッチが生まれる。RCW は GC の管理下にあるが、内包する COM オブジェクトが GC の対象でないので、結局、アプリケーション作成者が RCW を通じて COM オブジェクトが使用するメモリを、参照カウントというしくみで管理しなければならない、ということになる。

RCW に宿る参照カウント

前述したように、.NET の世界から COM オブジェクトにアクセスするには、それをラップした RCW を通じて行うのだが、COM オブジェクト上の参照カウントというのは、プライベートなメンバなのである。

つまりどういうことかというと、RCW は、COM オブジェクト上の参照カウントを直接見ることはできないということであり、それはすなわち、RCW 上にも参照カウントを持たなければならない、ということを意味する。見えないのだから、別立てで、擬似的に管理してやるしかないというわけ。

そう、.NET デベロッパーは、この RCW 上の参照カウントを管理してやるのだ。そして、これが非常にめんどくさい。

Excel における COM オブジェクト

ここからは、実例に沿って説明した方がわかりやすいので、みんな大好き Excel くんにご登場願おう。

レガシー VB では、自動参照カウンタなるものが提供されるので、たとえば、hoge.xls を開いて、1 つ目のシートの A1 セルの値を表示しようと思えば、以下のように書けた。VB6 は手元にないので、VBScript / WSH で書いてみる。

Sub ShowRangeA1()
Dim excel
Dim book
Set excel = CreateObject("Excel.Application")
Set book = excel.Workbooks.Open("hoge.xls")

Dim a1val
a1val = book.Sheets(1).Range("A1").Value

book.Close False
excel.Quit

WScript.Echo a1val
End Sub

これは、参照カウントを VBscript 側で勝手に管理してくれるからこそ可能な書き方である。どういうことか。まずは、セル(Range)を扱うまでに必要となる、Excel が内包する COM オブジェクトの階層構造を知らなければならない。

  • Application
  • Workbooks
  • Workbook
  • Worksheets
  • Worksheet
  • Range

Excel アプリケーションがあって、Excel は MDI だからアプリケーションごとに複数のブックを持っていて、その中から必要なブックを指定し、ブックの中には複数のシートがあるから、そこから特定のシートを指定し、そしてシート上の多数のセルから任意のひとつを指定し、その値を得る。セルの値を取ってくるということは、つまりそういうことであり、上記の要素、その一つひとつがすべて COM オブジェクトなのである。

参照カウント プログラミング

そう、上記に挙げた COM オブジェクトごとにそのラッパーである RCW も存在し、そして、それぞれに参照カウントを持っていて、それらすべてを管理しなければいけないのだ。それが、.NET で COM を扱うということなのだ。文章だけではわかりづらい。ともかく、上記のコードを C# で書き換えてみることとしよう。

public void ShowRangeA1()
{
Excel.Application xlApp = new Excel.Application();
Excel.Workbooks xlBooks = xlApp.Workbooks;
Excel.Workbook xlBook = xlBooks.Open(@"hoge.xls",
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing, Type.Missing, Type.Missing,
Type.Missing, Type.Missing);
Excel.Sheets xlSheets = xlBook.Worksheets;
Excel.Worksheet xlSheet = (Excel.Worksheet)xlSheets[1];
Excel.Range xlCells = xlSheet.Cells;
Excel.Range xlRangeA1 = (Excel.Range)xlCells[1, 1];

string a1val = (string)xlRangeA1.Value2;

Marshal.ReleaseComObject(xlRangeA1);
Marshal.ReleaseComObject(xlCells);
Marshal.ReleaseComObject(xlSheet);
Marshal.ReleaseComObject(xlSheets);
xlBook.Close(false, Type.Missing, Type.Missing);
Marshal.ReleaseComObject(xlBook);
Marshal.ReleaseComObject(xlBooks);
xlApp.Quit();
Marshal.ReleaseComObject(xlApp);

MessageBox.Show(a1val);
}

管理するとは、つまり、RCW をすべて変数に保持しておくということであり、そしてその参照カウントをデクリメントするのが Marshal.ReleaseComObject メソッドである。見るからにめんどくさい。処理の本質ではないコード、タイプ量が激増している。

が、これだけでも十分めんどくさいのに、実は、例外発生時にも確実にデクリメントを行うためには、RCW を生成するごとに、try finally してやらなければいけないのである。詳しくはじゃんぬねっとさんの記事を参考にしていただきたいが、こうなると、超絶ネスト構造の一丁上がりである。

そんなの絶対に書きたくない。なにがあろうと絶対にイヤである。しかるに結局、じゃんぬねっとさんが書かれているように、もし COM オブジェクトを扱うのなら、COM 専用の言語で、つまり、自動参照カウンタが提供される言語で実装するのがベターだ、ということになるのである。

他にも実装の方法はいくつかあるようなのだけれど、いまの僕にはちょっとハードルの高いお話になっているので、その辺は今回は割愛ということで。

参考記事

関連記事

Viewing all 26 articles
Browse latest View live