Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Funny local variable behavior


  • Please log in to reply
14 replies to this topic
fincs
  • Moderators
  • 1662 posts
  • Last active:
  • Joined: 05 May 2007
Just run this (I understand it's for performance but at least it should be documented):
Loop, 2
	func()

func()
{
	; Note no static bugvar!
	static switchvar = 0
	if switchvar
	{
		; Make the variable show its hidden contents
		NumPut(Asc("H"), bugvar, 0, "UChar")
		MsgBox % bugvar
	}else ; This function was ran for the first time
	{
		; Initialize the variable
		bugvar = _idden variable contents reappear!
		switchvar = 1
	}
}


jaco0646
  • Moderators
  • 3165 posts
  • Last active: Apr 01 2014 01:46 AM
  • Joined: 07 Oct 2006
I get a blank message box (running the latest AHK version).

MasterFocus
  • Moderators
  • 4322 posts
  • Last active:
  • Joined: 08 Apr 2009

I get a blank message box (running the latest AHK version).

Same here.

fincs
  • Moderators
  • 1662 posts
  • Last active:
  • Joined: 05 May 2007
I'm also using the vanilla version of AutoHotkey v1.0.48.05 (no Unicode/_L stuff involved). Am I missing something here?
Posted Image

Roland
  • Members
  • 307 posts
  • Last active: Mar 09 2014 07:55 PM
  • Joined: 08 Jun 2006
It's the #NoEnv directive that's causing this to happen :shock:

jaco0646
  • Moderators
  • 3165 posts
  • Last active: Apr 01 2014 01:46 AM
  • Joined: 07 Oct 2006
Confirmed.

MasterFocus
  • Moderators
  • 4322 posts
  • Last active:
  • Joined: 08 Apr 2009

A future version of AutoHotkey might make this behavior the default.

:|

jballi
  • Members
  • 1021 posts
  • Last active:
  • Joined: 01 Oct 2005
Unless I'm missing something, this is a documented condition (feature) and it includes local variables that are defined within functions. fincs just exposed a means of getting at memory that has not been released. You can duplicate this "problem" with global variables. Just create a global variable to something with a length that is less than 64 bytes, set the variable to null, and then use fincs' trick for reallocating the memory. Something like this:

#NoEnv
gVar=_idden va0iable con0ents reap0ear!56789012345678901234567890123
VarSetCapacity(gVar,0)
NumPut(Asc("H"),gVar,0,"UChar")
MsgBox % gvar
For all (OK, most) practical purposes, the variable does not exist when you use it again. The string length is zero and if you test the variable, i.e. If MyVariable, it will test as False. All string commands such as StringLeft, StringLower StringMid, etc. are not affected.

Them be my thoughts...

infogulch
  • Moderators
  • 717 posts
  • Last active: Jul 31 2014 08:27 PM
  • Joined: 27 Mar 2008
What's happening here is that the first character of the discarded variable is set to NULL, and since ahk's strings are null-terminated this essentially makes the string a length of 0. By overwriting the null character with another character, the remaining string is visible again until the next null character. (which is now at the end of the first string because, as jballi mentioned, it's length is < 64 bytes and it is never really freed in the true sense)

I think.

:p

Edit:

"All string commands such as StringLeft, StringLower StringMid, etc. are not affected."

This, too can be explained by the fact that ahk keeps a separate record of the length of the string in a given variable, which is set to 0 when cleared. All the string commands refer to this value for validating values, which can save it a lot of time vs going through the entire string to find the null char every time someone uses a string command.

Lexikos
  • Administrators
  • 9449 posts
  • Last active:
  • Joined: 17 Oct 2006
What do you want to happen when you use NumPut on an "uninitialized" variable? Variables are not guaranteed to be zero-initialized unless you specifically do it yourself.

It's the #NoEnv directive that's causing this to happen

If #NoEnv is not present, AutoHotkey looks for an environment variable when the variable's internal "length" field is 0. Even if there is no environment variable, it does not use the variable's actual contents in that case. #NoEnv itself is not in any way responsible for the effect demonstarted by fincs and jballi.

OT: #NoEnv is important for performance because without it, every time you evaluate an empty variable, the script has to search for an environment variable by the same name. Even having this mechanism present (with or without #NoEnv) negatively affects performance of all scripts, though perhaps not noticably.

fincs
  • Moderators
  • 1662 posts
  • Last active:
  • Joined: 05 May 2007

What do you want to happen when you use NumPut on an "uninitialized" variable? Variables are not guaranteed to be zero-initialized unless you specifically do it yourself.

Just look at this case (I only want it to be documented!):
DecodeBase64(data){
	DllCall("crypt32\CryptStringToBinaryA", "uint", &data, "uint", 0, "uint", 1, "uint", 0, "uint*", len, "uint", 0, "uint", 0)
	VarSetCapacity(d_data, len)
	DllCall("crypt32\CryptStringToBinaryA", "uint", &data, "uint", 0, "uint", 1, "str", d_data, "uint*", len, "uint", 0, "uint", 0)
	; This function doesn't work properly if you don't uncomment the NumPut() line.
	; The scripter may be puzzled to see remnants from the last function result if
	; the base64-encoded data doesn't contain a termination NULL (the most common case):
	;NumPut(0, d_data, len, "UChar")
		
	return d_data
}


Lexikos
  • Administrators
  • 9449 posts
  • Last active:
  • Joined: 17 Oct 2006
I think you missed my point.

"All local variables start of blank each time the function is called," but this only means a null-terminator is set at the beginning of the variable. Any content following the null-terminator is initially undefined, so relying on its value is asking for trouble. Documenting that local variables might "retain" their contents between calls would be counterproductive since the contents are still undefined in other situations.

On the other hand, it might be worth clarifying how variables are "initialized". VarSetCapacity hints at it:

FillByte: This parameter is normally omitted, in which case the memory of the target variable is not initialized (instead, the variable is simply made blank as described above).

Local variables are freed or cleared when the function returns in the same way as VarSetCapacity(var,0), so the following applies:

For performance reasons, freeing a variable whose previous capacity was between 1 and 63 might have no effect because its memory is of a permanent type.

However, it only might have no effect, so you can't rely on it. If d_data extends beyond 63 bytes, it becomes non-permanent and will be freed when the function returns. Undocumented: In all subsequent calls it will receive non-permanent memory. VarSetCapacity will allocate exactly the length requested (plus 1) and will null-terminate in the correct position every time. This behaviour can be forced:
DecodeBase64(data){
    static s
    if !s  ; To demonstrate it only needs to be done once.
        s := VarSetCapacity(d_data,64), VarSetCapacity(d_data,0)
    ...
}
Btw, even if the variable's contents are freed, there's no guarantee that the same memory won't be allocated to that variable (or a different variable) at some later point. A variable could hypothetically even be allocated different memory with contents identical to what it had previously.