マクロ終了時にコールバックされる関数のNo.08171
天翔記jp さん 16/08/09 23:51
 
こんにちは、天翔記.jpです。

■概要

「マクロ終了直前」に呼び出されるコールバック関数を登録する仕組みが必要

といった話となります。


■問題発生例
#dll = loaddll( "test" ); // test.dllの呼び出し

・ このあたりの処理で、dll内部の関数を使ってマクロで処理を行う。
・ マクロ内で並列的なスレッドが構築されている
・ もしくはシングルスレッドアパートメント「スレッド」が秀丸とは別に展開され
ている
・ もしくはマルチスレッドアパートメントが展開されている

・・・

freedll(#dll); // 当人は明示的に解放したつもり。
  // しかし、実態はFreeLibrary であり、
    // よほど単純なdllでもない限り、ここでこのように記述したとしても、このタ
イミングでは解放が実行されることはない。
   // (=ある程度のプログラムであれば、FreeLibrary呼び出しのタイミングではプ
ロセスからのデタッチがなされない方が普通)



■問題点
複雑な処理や並列タスク、秀丸と並列スレッドの関係となるウィンドウなどを出すマ
クロを実行すると、
マクロ実行中であっても「秀丸」を突如閉じることが出来るため、
受け身が取り切れず、プログラムの後片付けが極めてやりにくい
(内部Exceptionを受けて、あわててパタパタDisposeする感じ)


■提案
マクロが終了直前に呼ばれるサブルーチンを登録する関数を用意する
(イベントハンドラ的にサブルーチンをとして結びだせる仕組み窓口でもあります)

// マクロ例
// どこかで、マクロ終了直前に呼び出されるイベントハンドラ的なサブ
ルーチンを登録
// Prevは前、Postは後
add_eventhandler("OnEndMacro", "Prev", "my_endcmacro1");
add_eventhandler("OnEndMacro", "Prev", "my_endcmacro2"); // 同じタイミング(E
NDMACROのPREV)で複数登録されたら、登録した順番でそのまま呼び出す

endmacro;
my_endmacro1:
    // 何かその人にとって重要で必ず行う必要がある後片付けの処理
    return;

my_endmacro2:
  // 別系列の何か処理
    return;


みたいな感じです。

ご検討ください。


[ ]
RE:08171 マクロ終了時にコールバックされNo.08174
秀丸担当 さん 16/08/10 16:24
 

マクロはEscキーなどで中断できたりして、どちらかというと厳格ではなく緩い
ほうと思います。
Escキーでマクロ中断して、さらに終了用のマクロが呼ばれるということになる
と、ややこしいことになりそうです。
中断のことを考えなくていいのであれば、現状の、普通にendmacroの前に終了処
理をするだけと変わりない気がします。

しいてなんとかするとすれば、マクロ終了でdll内の特定の関数が呼ばれるよう
にするのであれば、ありかとは思います。(例えばENDMACRO();という関数)

または、COMの場合はcreateobjectに対して、keepobjectという文があり、これ
で自動的に解放する方法を指示できます。
loaddllの場合も自動的な解放を指示できたらいいと思います。

loaddllしてロードされるdllは、loaddll "user32.dll";などのどこからでも
ロードされるものでない限り、ご自身で作られるものであれば、freedllで
FreeLibraryされ、DllMainで制御は可能だと思います。
確実なものを求めるのであれば、やはりDllMainがいいのではないかと思います
が、どうでしょうか。

[ ]
RE:08174 マクロ終了時にコールバックされNo.08179
天翔記jp さん 16/08/11 02:53
 
お返事ありがとうございます。

具体的な動作するミニマルなサンプルがあった方が、
「何が問題か」がわかりやすいということで、用意しました。

http://天翔記.jp/?page=nobu_tool_hm_tantou_kaisetsu_01

■問題点1

 ・マクロが実行され、freedll(#dll)したとしても、
  wpfhide.cpp内の「DLL_PROCESS_DETACH」に対応する部分は
  呼び出されることは無いハズです。

  該当の秀丸プロセスが解放された時点で、はじめて「DLL_PROCESS_DETACH」が呼
び出されることを、
 デバッグモニター等を利用すれば、お手元で確認できるかと思います。


■問題点2

 ・該当のWPFウィンドウを表示している間、マクロは実行中なわけですが、
  『秀丸は即座に普通に×ボタンなどで閉じることが出来るハズ』です。
  これ自体は、自由度があるので良い挙動ともいえますが、
  閉じた際に、秀丸からdllへと何の伝達もないため、
  誰が「MyWindows::f」を正しく解放するのか、
  という問題が出ます。

  現状では、MyWindows::f」はOSが結果として正しく解放することもあれば、(dll
を呼び出していたハズの本体が先に消えてしまって間に合わず)
  不正終了することもあります。
  まぁ五分五分でほとんど運次第といったところでしょうか。


■問題3(DllMainが利用不可能という問題)

 ・「DllMain」は特別な役割があるため、
  マネージドアセンブリが含まれる関数が1つでも含まれていれば、
   直接間接にかかわらず、一切実行することは出来ません。
  これはマイクロソフト社のVC++コンパイラの「仕様」です。

  case DLL_PROCESS_DETACH:
     MyWindows::f->Close();
        // Close()待ち
    MyWindows::f = nullptr;

  のような記述は許されません。
  (コンパイルも通りませんが、仮にコンパイラが認識できないように
  関数やdllを跨いで、遠く関節で呼び出しても不正エラーになるだけです)


■焦点

 このため、最初の投稿となります。
 焦点は3つです。

 @順当にマクロが全て走った場合に問題が無いようにする必要がある。
  マクロ内でfreedllという記述自体はあるべきだ、ということです。

 Aマクロ実行中に秀丸を突如とじた場合、dllに如何に伝達するか
 B Aの伝達は、freedllよりは早くする伝達する必要があります。


■書いてるうちに整理できた…かな?

 そうなると、結局必要な案件としては、
 「マクロ実行中に(強制ではなく)普通に「秀丸を閉じた」ときどうするか」だけを
焦点とすればよさそうです。

 そうなると解決する対象は、
 「DllMainからは呼び出せない」という一点ですので、
 普通のC++/CLIの王道通り、

 FreeLibraryする直前に、「DestructDLL」みたいな関数がdll内に定義されていれば、
 それを呼び出します、とかでよさそうです。
 (FreeLibraryとは別途呼び出す理由は、あくまでもDllMainという特殊な関数からは
 マネージドdllが含まれたdllを直接・関節に関わらず呼び出せない」)
 一点を片付けるためだけなのですから。

 もちろんマクロでFreeLibrary直前に呼び出す関数を定義してもらうのもありだと
思います。

  #dll = loaddll( "wpfhide" );
 regist_prev_call_freelibrary( #dll, "DestructFunc" );

 ・・・記述・処理@

 freedll(#dll);

 
■途中で秀丸を閉じた場合
  regist_prev_call_freelibraryに基づき、
 処理@ぐらいのタイミングで秀丸を閉じたとしても、
 DestructFuncが呼び出されれ、そして、FreeLibraryが呼び出される。

■最後までマクロが実行された場合
  regist_prev_call_freelibraryに基づき、
   
 freedllの前に、秀丸が DestructFunc を呼び出す。


いずれにしても、単純なネイティブだけの時代とは異なり、
今のdllは「単体呼び出し」でもかなり癖のある挙動をしますので、
制作サイドで良く揉んでいただければと思います。

 

 

[ ]
RE:08179 マクロ終了時にコールバックされNo.08182
天翔記jp さん 16/08/11 14:34
 
以下が一番妥当性が高いように思いました。

@ マクロ内で loaddll をした際、
  該当のdllに

  extern "C" __declspec(dllexport) void __cdecl DllEnsureInit(void)

  がexportされていれば、秀丸はloaddllに続いて、それを呼び出す。


A マクロ内で freedll した際、
  該当のdllに

  extern "C" __declspec(dllexport) void __cdecl DllForceTerm(void)

  がexportされていれば、秀丸は先にDllForceTermを呼び出したあと
  FreeLibraryをする。


B 秀丸が閉じる際、対象のdllに対し、「Aの処理」をまだ実行していないのであれ
ば、
 Aの処理を行う。
 
 但し、マクロ関数を通して、明示的にdllをkeepする(解放しない)指示が
 なされている場合は、行わない。



これでいかがでしょうか。

DllMainで出来ること、呼び出せることが、制限が大きいため、
結局個人レベルでもdllを呼び出すメインプログラム側から普通は@ABのようなこ
とを行っていますので、
秀丸でもその形を踏襲するのがわかりやすそうです。


ご検討ください。
 

[ ]
RE:08182 マクロ終了時にコールバックされNo.08184
秀丸担当 さん 16/08/12 11:38
 

テスト用のプログラムありがとうございます。
確かにfreedllでは解放されないことが確認できました。
.net側からもDLLを参照している扱いのようで、DLLの参照カウントが上がってい
るのだと思います。
もう一回FreeLibraryするか、DLLを二段構えにするかなどで回避はできると思い
ますが、いずれにしても.netはデタッチのタイミングで呼ばれることは危険なよ
うなので、事前に終了処理が呼ばれる仕組みがあったほうがいいと思います。

気になったのが、マクロ実行中に本体が操作可能な状態になっていて、これはあ
まり良くないと思います。
メッセージボックスやダイアログボックスのように、何らかのウィンドウが出て
いるときは、本体は操作できないようになっているほうがいいです。
メッセージループがDLL内で回っていることになっていて、DLLの関数内で秀丸エ
ディタ本体が終了することも問題で、他にも本来処理されるべきものが処理され
ない可能性があります。
例えばマウスの中ボタンを押したときの処理がうまく動いていないです。

DLL内で秀丸エディタ本体が終了するのは良くないので、マクロ実行中は終了さ
せないような対策も考える必要がありそうです。

できれば、EnableWindowでトップレベルのウィンドウをDisable状態にされるの
が、本来あるべき順序だと思います。

一方で、マクロ終了後も残るようなシングルスレッドのインスタンス、あるいは
マルチスレッドの終了処理のためにも、FreeLibraryの直前に呼ばれる関数も検
討したいと思います。
固定名より自由な名前を設定できたらいいと思います。
(例えばsetdlldetachfunc #dll, "funcname";とか)

[ ]
RE:08184 マクロ終了時にコールバックされNo.08185
天翔記jp さん 16/08/12 18:33
 
>気になったのが、マクロ実行中に本体が操作可能な状態になっていて、これはあ
>まり良くないと思います。

確認ありがとうございます。
今回は最小サンプルでしたので、「解放されない」ということにのみ主眼をあてたも
のでした。

ウィンドウ表示に限れば、「表示されてる間は秀丸を触らせない」などは、
単純にフォームをモーダルウィンドウとして表示すればよく、
例えば、「Windows.Forms.Form」の「ShowDialog()」を実行するだけで、
秀丸担当さんがおっしゃるような状態になります。

>一方で、マクロ終了後も残るようなシングルスレッドのインスタンス、あるいは
>マルチスレッドの終了処理のためにも、FreeLibraryの直前に呼ばれる関数も検
>討したいと思います。
よろしくお願いします。

>固定名より自由な名前を設定できたらいいと思います。
>(例えばsetdlldetachfunc #dll, "funcname";とか)
そうですね。
「8179」の最後で投稿させていただいたのと同じで
名前を設定できて良いと思います。

あと、可能なら
「デフォルトの名前も決まっていて、デフォルト名でdllに定義してあれば、自動で
呼び出しますよ」
というようになっていると「dllを作る側」「それを使う側」ともに
便利かなと思いました。

・「dllを作る側」にとっては、「秀丸8.xx以降なら○○を明示的に呼び出して〜、
それ未満なら呼び出さないで〜」という記載が不要
・「dllを使う側」からしたら、「マクロで明示的にsetdlldetachfunc」しなくても
よいため

結局は、「freedll」されるハズのタイミングで、解放的な挙動をしていない、する
べきだ、ということですので。

[ ]
RE:08185 マクロ終了時にコールバックされNo.08285
秀丸担当 さん 16/08/25 16:51
 

V8.66β1で、setdlldetachfunc文を追加してみています。
デフォルトの関数名は用意していないです。
あったほうがいいと思いますが、やるとしたら既存のDLLにある関数名とかぶら
ない名前でないといけなくて、いい名前が思いつかないです。

秀丸本体のエクスポートされた関数の Hidemaru_GetTotalTextUnicode() などに
対して、DLLは HidemaruDll_Detach() とか。
浮動小数点数版用にFLOATMACRO()というダミー関数にならって
HIDEMARUDLLDETACH()とか。
こういう名前にするか考え中です。

それか、WindowsのEXEで採用しているmanifestのごとく、GUIDが記述されていれ
ば対応している意味にして、単純な名前でもありかとは思います。
例えばGUIDを模した、
ABCD1234_ABCD_ABCD_ABCD_ABCD12345678()
というダミー関数があれば、
DllDetach()
という単純な関数名をサポートしているとみなして、それを呼び出すとか。
ただ、たぶん使われる人はかなり少ないと思うので、そこまでするのも大げさと
いう気もします。

[ ]
RE:08285 マクロ終了時にコールバックされNo.08286
天翔記jp さん 16/08/25 17:24
 
>V8.66β1で、setdlldetachfunc文を追加してみています。
実装ありがとうございます。
後ほど、挙動を確認させていただきます。

>あったほうがいいと思いますが、やるとしたら既存のDLLにある関数名とかぶら
>ない名前でないといけなくて、いい名前が思いつかないです。
そうですね〜、
おっしゃっているように
「Hidemaru」等接頭を付ける形にしておけば、
被りの可能性というのは、現実的には排除出来ていると思います。

[ ]
RE:08285 マクロ終了時にコールバックされNo.08319
天翔記jp さん 16/09/04 08:27
 
こちらですが、やはり、「デフォルトでの関数名」も決まっていたほうがよさそうで
す。

例えば、
8.64以前までの記述として、

■8.64までの記述
----------------------------------------------------
#dll = loaddll( hidemarudir + @"\hmPy.dll" );

#r = dllfunc( #dll, "DoFile", hidemarudir + @"\a.py" );

#_ = dllfunc( #dll, "DestroyScope" ); // ここがターゲット
freedll(#dll);
----------------------------------------------------

これが
■8.66以降での記述
----------------------------------------------------
#dll = loaddll( hidemarudir + @"\hmPy.dll" );
setdlldetachfunc #dll, "DestroyScope";

#r = dllfunc( #dll, "DoFile", hidemarudir + @"\a.py" );

freedll(#dll);
----------------------------------------------------
となります。

8.64までと比べると、マクロ途中での秀丸の終了にも対応しているため、
機能的に向上していますが、
「dllを使用する側(マクロ記述側)」が、明示的に「(今回の例の)DestroyScope」を
設定する意味は、薄いと感じます。

使用者側ではなく、原則的にはdll制作側と秀丸とで、名前を取り決め、
暗黙裡に呼び出した方が、「dll製作者」にとっても「dll利用者」にとっても、「使
用間違い」発生せず、お互いにありがたいハズです。

■本来あるべき姿
----------------------------------------------------
#dll = loaddll( hidemarudir + @"\hmPy.dll" );

#r = dllfunc( #dll, "DoFile", hidemarudir + @"\a.py" );

freedll(#dll); // この記述で(デタッチされなかったとしても)必要な後片付けはぐ
らいはするようdll製作者が組んでおく、秀丸はそれをfreedllの一環として呼び出す
----------------------------------------------------


■呼び出す関数名の名前
「Hidemaru_Evh_DllProcessDetach」
とかでいかがでしょう。

これなら既存のものと、名前が被らないことが期待できると思います。
(もし将来addeventhandlerとかに切り替えた際にも、対関係が構築しやすそうです)

[ ]
RE:08319 マクロ終了時にコールバックされNo.08320
秀丸担当 さん 16/09/05 10:27
 

何か適当な名前を考えてやるつもりだったのですが、その矢先、
OleUninitialize()の前のFreeLibrary()で落ちるケースあるという例があったこ
とで、慎重に考えています。
このあたりは非常にデリケートだと思います。

V8.66β3現在、loaddll文は自動的なFreeLibraryをして、loaddll()関数は放置
しています。
変ですが、変なまま、できるだけ従来のものは何も変わらないようにしています。
setdlldetachfuncをした場合は、常に放置です。

挙動が難解で、作る側も使う側も、いま理解していても、将来も忘れずに理解し
ているかどうか心配になってきました。
将来でもパッと見てわかるように、
AutoDllDetach_After_Hm866
というような名前にしたらいい気がしてきました。

[ ]
RE:08320 マクロ終了時にコールバックされNo.08322
天翔記jp さん 16/09/05 13:28
 
なんだか、書き足しているうちに長くなってしまいました…


>V8.66β3現在、loaddll文は自動的なFreeLibraryをして、loaddll()関数は放置
>しています。

以前からの秀丸に、このFreeLibraryの扱いの違いがあったらな、

loaddll文は、@:「マクロ内で一時的にdllを呼び出す」みたいな行儀良い使い方が
中心だったのが、

loaddll関数は、@のみならず、A:「該当マクロではなく、該当プロセスに対する寄
生プログラム」
みたいな使い方もするようになっていくことにも繋がったであろうと思います。
(規模が大きいほど、寄生しておいた方が実行速度も速いし、裏でJITが動くものだと
寄生しておいた方が効果がでかいので)

>挙動が難解で、作る側も使う側も、いま理解していても、将来も忘れずに理解し
>ているかどうか心配になってきました。

なんとか理屈を付けてでも、「なぜそうなのか?」をの方針を打ち立てておかれた方
がよろしいかと思います。
私の理解では以下のような感じですが、やはり  setdlldetachfunc の理由付けは若
干苦しいですねぇ〜

a) loaddll文は、「秀丸で出来るだけ自動的にコントロール」することを旨としてい
る。
 例えば、
 loaddll文は、「それ自体が1つ前に呼ばれていたdllに対してfreedllを明示的に
記述したこと」と同一の意味も併せ持つため、
 新たなloaddll文が呼ばれると、1つ前のロードしたものは自動で解放される。(lo
addllは1つ前のdllへの明示的な解放を兼ねる)
 マクロ終了までに解放し忘れていた場合などでも、マクロ終了直前に解放される。
(秀丸による暗黙的な解放)

b) loaddll関数は、秀丸マクロ記述者側が明示的に複数のdllの読出しや解放(のタイ
ミングも含めて)を
 責任をもってコントロールしたい場合に利用する、
 よって、明示的にfreedllした場合に限ってFreeLibraryされ、freedllしなかった
場合は、「OSによる解放」にゆだねられる。
 よって、「秀丸による暗黙の解放」は行われない。

c) setdlldetachfunc
loaddll文、もしくは、loaddll関数によって呼び出されたdllに対し、「FreeLibrary
実行の直前に呼ばれる関数」を指定できる。
もしも、「明示的な解放(freedll含む)」がない場合、等の理由で(該当プロセスの)
秀丸終了直前までに、
該当のdllに対するFreeLibraryが行われなかった場合、そのタイミングで、「指定の
関数」の呼び出しが試みられる。

なお、副作用として、setdlldetachfunc は、「秀丸による暗黙的な解放」が「OS
による解放」に変化してしまう。
 └ ※現状こうなっているなら、やはりここは苦しいデスね...

本来なら
・「setdlldetachfunc関数」で付けた分は、(明示・暗黙含め)FreeLibraryが呼ばれ
たら、その直前。FreeLibraryが呼ばれなければ、OS解放に対応するものとして、対
象の秀丸の終了直前。
・「setdlldetachfun文」で付けた分は、(明示・暗黙含め)FreeLibraryが呼ばれたら、
その直前。FreeLibraryが呼ばれなければ、該当マクロの終了直前。

というのが本筋とは思います。


>AutoDllDetach_After_Hm866
>というような名前にしたらいい気がしてきました。
setdlldetachfunc文やsetdlldetachfunc関数が、
loaddll文とloaddll関数の違いに引っ張られて定義や伝達が苦しいようなら、
setdlldetachfunc撤回して、

AutoDllDetach_After_Hm866 などの一本に絞るのもありかと思います。

(明示的だろうと、暗黙的だろうと、FreeLibraryが呼ばれるタイミングで直前に呼ぶ
よ、
最後までFreeibraryが呼ばれてないようなら、秀丸終了時に1回呼ぶよ、ぐらいの)

FreeLibraryのタイミング自体を変更してしまうと、β1のように非常に大きな影響
がありますが、
「FreeLibraryのタイミングは変わらず」、その直前に1つ関数呼び出しますよ、
っていうのは、特に既存の仕組みに影響を与えないと思います。



>飛んでしまう
飛んでしまっていた原因は、
「dllを解放するタイミングが早くなると、秀丸が関節的に呼ぶことになる先のdllの
展開実体が無くなるため」
のひとことに尽きると思います。

寄生前提のdllだとものによってはこうなってしまいますので…

[ ]
RE:08322 マクロ終了時にコールバックされNo.08325
秀丸担当 さん 16/09/05 15:20
 

秀丸エディタが暗黙的に解放するか放置するかというのは、実際はほとんど違い
が無いです。
暗黙的な解放というのは、マクロ終了時ではなく、プロセスの終了(WinMainの
終わり)のことです。

WinMainの最後が以下のようになっているか
    FreeLibrary( hmod );
    OleUninitialize();
    return 0;
  }

それとも以下のようになっているか
    OleUninitialize();
    return 0;
  }//この後OS(?)によってFreeLibrary

というただそれだけの違いで、落ちるかどうかが気になっているだけで、機能の
違いはないはずです。
秀丸エディタが起動中の間は、freedllさえしなければ、どのケースであっても
全部ロードされっぱなしです。

名前がころころ変わってすみませんが、AutoDllDetach_After_Hm866 だと自動的
にデタッチさせるみたいな意味に感じてしまう気がしたので、
DllDetachFunc_After_Hm866 という名前にしようかと思います。
この関数がエクスポートされていれば、loaddllの直後に
setdlldetachfunc #dll "DllDetachFunc_After_Hm866"; をしたのと同等という
ことにしようかと思います。

[ ]
RE:08325 マクロ終了時にコールバックされNo.08326
天翔記jp さん 16/09/05 22:46
 
>WinMainの最後が以下のようになっているか
>    FreeLibrary( hmod );
>    OleUninitialize();
>    return 0;
>  }
>
>それとも以下のようになっているか
>    OleUninitialize();
>    return 0;
>  }//この後OS(?)によってFreeLibrary
なるほど〜
そこまで最終付近で、かつ直前であれば、
影響は一見すると無いようにも思えます。

そういえば、β1で明示的にFreeLibraryする変更が入った際に
落ちていたのは、
プロセス内をフックしたもののうちでも、
「DestroyWindow」をフックしたものでした。

「DestroyWindow」以上にトラブりやすいと思える
「SendMessgeA/SendMessageW」すら落ちなかったので、
Win7ぐらいまでは、OleUninitializeは内部的に(あまり関係なさそうに思える)
DestroyWindowを巡り巡ってcallするのかもしれないですね。
(Win8/Win10では別実装になっているのかな?)




[ ]