Provides an effective way to debug a script's use of built-in functions.
; Requires AutoHotkey v1.0.48 and the appropriate version of LowLevel, ; which can be found here: http://www.autohotkey.com/forum/topic26300.html /* Function: DebugBIF Registers a user-defined function to be called when a built-in function either sets ErrorLevel to a non-zero value or returns an empty string, as appropriate. Syntax: DebugBIF( [ func_to_debug, func_to_call, event_info ] ) Parameters: func_to_debug - The name of the built-in function to debug. If omitted or empty, all applicable functions are affected. func_to_call - The name of a user-defined function to call. If omitted or empty, DebugBIF_DefaultHandler is used. event_info - An integer to pass via A_EventInfo. If omitted or empty, A_EventInfo contains a boolean value indicating whether ErrorLevel triggered the current call. */ DebugBIF(func_to_debug="", func_to_call="", event_info="") { static DebugErrorLevel, DebugResult, DebugInitd if func_to_call = func_to_call = DebugBIF_DefaultHandler if func_to_debug = { if DebugInitd return func_to_debug = RegExMatch,RegExReplace,NumGet,NumPut,GetKeyState ,Mod,ASin,ACos,Sqrt,Log,Ln,RegisterCallback,DllCall Loop, Parse, func_to_debug, `, DebugBIF(A_LoopField, func_to_call) DebugInitd := true return } ListLines Off if func_to_debug in DllCall,RegExMatch,RegExReplace use_error_level := true else use_error_level := false if event_info = event_info := use_error_level LowLevel_init() if !(bif := __findFunc(func_to_debug)) || !NumGet(bif+49,0,"char") || !(callback := RegisterCallback(func_to_call,"C F",4,event_info)) { ListLines On return } bif_native := NumGet(bif+4) if !VarSetCapacity(DebugErrorLevel) { VarSetCapacity(DebugErrorLevel, 56), NumPut(0xC35D5E5F, NumPut(0x10C4830C, NumPut(0x55FF5756, NumPut(0x1C75FF20, NumPut(0x75FF0E74, NumPut(0x3038800C, NumPut(0xC4830840, NumPut(0x8B08458B, NumPut(0x1055FF56, NumPut(0x1C75FF3E, NumPut(0x8B2075FF, NumPut(0x5718758B, NumPut(0x56EC8B55, NumPut(&DebugErrorLevel+4, DebugErrorLevel)))))))))))))) VarSetCapacity(DebugResult, 60), NumPut(0x0000C35D, NumPut(0x5E5F10C4, NumPut(0x830855FF, NumPut(0x57561875, NumPut(0xFF1C75FF, NumPut(0x0E750038, NumPut(0x80068B15, NumPut(0x7500087E, NumPut(0x830CC483, NumPut(0x0C55FF56, NumPut(0x1875FF3E, NumPut(0x8B1C75FF, NumPut(0x5714758B, NumPut(0x56EC8B55, NumPut(&DebugResult+4, DebugResult))))))))))))))) } if use_error_level __mcode(func_to_debug, "" . "68" __mcodeptr(bif_native) . "68" __mcodeptr(callback) . "68" __mcodeptr(__getVar(ErrorLevel)) . "FF15" __mcodeptr(&DebugErrorLevel) . "83C40C" . "C3") else __mcode(func_to_debug, "" . "68" __mcodeptr(bif_native) . "68" __mcodeptr(callback) . "FF15" __mcodeptr(&DebugResult) . "83C408" . "C3") ListLines On } DebugBIF_DefaultHandler(aName, aResultToken, aParam, aParamCount) { static IgnoreRegisterCallback_DllCall:=2 ListLines Off RealErrorLevel := ErrorLevel ; If the first two errors are RegisterCallback("DllCall"), assume it is due to the ; usual workings of LowLevel.ahk, and ignore them. if IgnoreRegisterCallback_DllCall if __str(aName)="RegisterCallback" && __getTokenValue(NumGet(aParam+0))="DllCall" { ListLines On return "", --IgnoreRegisterCallback_DllCall } else IgnoreRegisterCallback_DllCall := 0 ListLines ;FileAppend, BEGIN DEBUG CODE ~~~~~~~~~~~~~~~~~~~~, NUL s := __str(aName) Loop % aParamCount { t := NumGet(aParam+(A_Index-1)*4) sym := NumGet(t+8) s .= "`n [" A_Index "]: " if sym = 3 ; SYM_VAR s .= __str(NumGet(NumGet(t+0)+24)) " := " v := __getTokenValue(t+0) if (sym=0 || sym=3 || (sym=4 && !NumGet(t+4))) { StringReplace, v, v, ", "", All v = "%v%" } s .= v } v := __getTokenValue(aResultToken+0) if v != { sym := NumGet(aResultToken+8) if (sym=0 || (sym=4 && !NumGet(aResultToken+4))) { StringReplace, v, v, ", "", All v = "%v%" } s .= "`n ret: " v } else if !A_EventInfo s .= "`n`nReturned an empty string." if RealErrorLevel && A_EventInfo s .= "`n`nErrorLevel = " RealErrorLevel MsgBox, 16, %A_ScriptName% - Built-in Function Error, %s% ErrorLevel := RealErrorLevel ;FileAppend, ~~~~~~~~~~~~~~~~~~~~ END DEBUG CODE, NUL ListLines On }Example:
; Simplest usage: DebugBIF() ; Examples that should trigger a debug message: RegExMatch("abc", "(") ; Regex compile error. NumPut(0, "") ; Invalid second parameter. NumGet(var, 1) ; Invalid parameters. Mod(1,0) ; Divide-by-zero.The script contains code compiled from the following C++ source (plus some AutoHotkey type definitions):
typedef void (* DebugCallbackType)(char *aFuncName, ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount); MCODE_API void DebugBif1( Var *aErrorLevel // Pointer to ErrorLevel Var. , DebugCallbackType aOnErrorLevel // Callback to call if ErrorLevel != 0. , BuiltInFunctionType aBIF // BIF to call , void *aReturnAddress // Addressed pushed by expression evaluation, left on stack to reduce code size. , ExprTokenType &aResultToken // Token used to pass func name/receive result. , ExprTokenType *aParam[] // Parameters of this function call. , int aParamCount ) // Number of parameters. { // On entry, aResultToken.marker holds the name of the built-in function. // This value is overwritten within each built-in function, so save it now. char *bif_name = aResultToken.marker; // Call the built-in function. aBIF(aResultToken, aParam, aParamCount); // RegExMatch, RegExReplace and DllCall use non-zero ErrorLevel to indicate an error. if (*aErrorLevel->mContents != '0') aOnErrorLevel(bif_name, aResultToken, aParam, aParamCount); } MCODE_API void DebugBif2( DebugCallbackType aOnBlankResult // Callback to call if result is "". , BuiltInFunctionType aBIF , void *aReturnAddress , ExprTokenType &aResultToken , ExprTokenType *aParam[] , int aParamCount ) { char *bif_name = aResultToken.marker; aBIF(aResultToken, aParam, aParamCount); // Several functions use a blank result to indicate an error. if (aResultToken.symbol == SYM_STRING && !*aResultToken.marker) aOnBlankResult(bif_name, aResultToken, aParam, aParamCount); }
DllCallDebugger( ErrorCallbackName ) for AutoHotkey v1.0.47.06
DllCallDebugger hooks DllCall with machine code in order to detect non-zero ErrorLevels. Since it is implemented in native code which only calls back to script if an error occurs, it should have little effect on performance.
; Requires AutoHotkey v1.0.47.06 and the appropriate version of LowLevel, ; which can be found here: http://www.autohotkey.com/forum/topic26300.html DllCallDebugger(ErrorCallbackName="DllCallDebugger_DefaultHandler") { static ErrorCallback if ErrorCallback return if !(ErrorCallback := RegisterCallback(ErrorCallbackName,"C F",3)) return LowLevel_init() DllCallFunc := __findFunc("DllCall") DllCallBif := NumGet(DllCallFunc+4) ErrorLevelVar := __getVar(ErrorLevel) pbin:=__mcode("DllCall" ; void DllCall(ExprTokenType &aResultToken, ExprTokenType *aParam[], int aParamCount) ; { ; DllCallBif(aResultToken, aParam, aParamCount); , "FF74240C" ; push [esp+12] . "FF74240C" ; push [esp+12] . "FF74240C" ; push [esp+12] . "B9" DllCallDebugger_FormatPtr(DllCallBif) ; mov ecx, DllCallBif . "FFD1" ; call ecx ; if (*ErrorLevelVar->mContents != '0') . "B9" DllCallDebugger_FormatPtr(ErrorLevelVar) ; mov ecx, ErrorLevelVar . "8B09" ; mov ecx, [ecx] . "0FBE09" ; movsx ecx, byte ptr [ecx] . "83F930" ; cmp ecx, '0' . "7407" ; je +7 ; ErrorCallback(); . "B9" DllCallDebugger_FormatPtr(ErrorCallback) ; mov ecx, ErrorCallback . "FFD1" ; call ecx ; } . "83C40C" ; add esp, 12 . "C3") ; ret pbin2:=__mcode("DllCallDebugger_TokenValue" /* ExprTokenType *token = (ExprTokenType*) aParam[0]->value_int64; if (token->symbol == SYM_VAR) { aResultToken.symbol = SYM_OPERAND; aResultToken.marker = token->var->mContents; } else { aResultToken.symbol = token->symbol; aResultToken.value_int64 = token->value_int64; } */ , "8B4424088B088B018B50088B4C240483FA03750B428951088B108B028901C38951088B1089118B4004894104C3") ; WARNING: This will affect the entire page(s) containing our generated code. ; Make our code executable (for systems with Data Execution Prevention enabled): DllCall("VirtualProtect","uint",pbin,"uint",22,"uint",0x40,"uint*",0) DllCall("VirtualProtect","uint",pbin2,"uint",22,"uint",0x40,"uint*",0) } DllCallDebugger_FormatPtr(ptr) { FI := A_FormatInteger SetFormat, Integer, H ptr := RegExReplace(SubStr("00000000" SubStr(ptr+0, 3), -7), "(..)(..)(..)(..)", "$4$3$2$1") SetFormat, Integer, %FI% return ptr } ; token MUST be the immediate integer result of a math expression or built-in function. DllCallDebugger_TokenValue(token) { ; Implemented in machine code; see DllCallDebugger(). } DllCallDebugger_DefaultHandler(aResultToken, aParam, aParamCount) { ListLines Call = DllCall( Loop % aParamCount P:=NumGet(aParam+A_Index*4-4) ,V:=DllCallDebugger_TokenValue(P+0) ,T:=NumGet(P+8) ,Call.=(A_Index>1 ? ", ":"") . (T=1||T=2 ? V : """" V """") Call.=")" MsgBox DllCall failed with ErrorLevel = %ErrorLevel%`n`n Specifically: %Call% }Example:
; Initialize/set our callback. DllCallDebugger("OnDllCallFail") ; Should always succeed: DllCall("MulDiv","int",1,"int",1,"int",1) if ErrorLevel = 0 MsgBox DllCall succeeded ; Should always fail: DllCall("foo") DllCall("MulDiv","int",1,"int",1) OnDllCallFail() { MsgBox DllCall failed: %ErrorLevel% }For licensing terms, see Lexikos' default copyright license.
2009/09/27:
[*:3f23cumj]Updated for AutoHotkey_L31 compatibility.
[*:3f23cumj]Calling DebugBif() with no parameters a second time no longer attempts to re-hook the functions.
[*:3f23cumj]Added ListLines On/Off to prevent most of the debug code from appearing in ListLines.