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.




