LocalVar

From NSIS Wiki
Jump to navigationJump to search

So I was reading this forum post where JoeAcunzo had simulated creating local variables by creating a dedicated global Var for each possible "local" and using defines to reference the global. This is a pretty nice system for complex script libraries but it has a few flaws:

  • Recursive function calls would get their "local" variables corrupted.
  • One extra global Var for every possible local. This could amount to hundreds or even thousands of extra variables depending on the complexity of your script.
  • Have to call ${LocalVarEnd} for every ${LocalVar}.

New Implementation

Forum thread I have developed this system further to make use of the private stack plugin by Instructor and NSIS' own ability to generate text files during compilation. In my version of this system:

  • Recursive function calls work fine.
  • Only one global Var for each possible local in a given function. This means that, if your most complex function needs 10 local variables, then only 10 global Vars will be created.
  • You only have to call ${LocalVarEnd} once per function, with no parameters.

Usage

!macro Greet firstname lastname
	Push `${firstname}`
	Push `${lastname}`
	Call Greet
!macroend
Function Greet
	${LocalVar} param1
	${LocalVar} param2
	${LocalVar} salutation
	Pop ${param2}
	Pop ${param1}
	MessageBox MB_OK "${salutation}${param1} ${param2} has entered the building"
	StrCpy ${salutation} "Hello "
	MessageBox MB_OK "${salutation}${param1} ${param2}"
	${LocalVarEnd}
FunctionEnd

${LocalVar} and ${LocalVarEnd} must be called from inside a Function or Section. On entering a function and declaring a LocalVar, the initial value of the var will be an empty string, even if it is a recursive call to the function.

Cons

  • Calls to ${LocalVar} must not be run-time conditional, otherwise other functions' local variables will get corrupted. It is safest to make all calls to ${LocalVar} at the beginning of the function and the call to ${LocalVarEnd} at the end of the function.
  • You cannot call Return or Abort in a function before calling ${LocalVarEnd}, otherwise other functions' local variables will be corrupted. If you need to conditionally call Return you need to use the standard NSIS registers or one more dedicated global variable to temporarily hold the value of the local variable. Example:
	...
	Push ${param1}
	${LocalVarEnd}
	Exch $R0
	${If} $R0 == "Elvis"
		Pop $R0
		Return
	${Else}
		Pop $R0
	${EndIf}
FunctionEnd

NSIS script for the new implementation

Grab the code below and put it in its own script file (LocalVars.nsh). !include that script file early in your script so that the macros can be called from any Function/Section. During .onInit you need to !insertmacro LocalVars_Init

!ifndef DOLLAR
	!define DOLLAR `$`
!endif
!define LocalVar_Max "0" ;how many local variables in total were created. This is purely for interest - you can !echo this in your compile log.
!define LocalVar_MaxFunction "" ;the function that had the highest number of vars. Again for interest, this is so you can review functions that may be too complex.
!define LocalVar_Count "0" ;how many local variables have been used in the current function. This is a running total and is important for generating new global Vars.
 
Var LocalVar_Stack ;a dedicated global Var to hold the pointer to our private stack
!macro LocalVars_Init
	;called during .onInit to set up the private stack that will be used to save/restore local variables between function calls
	stack::_dll_create /NOUNLOAD
	Pop $LocalVar_Stack
!macroend
!macro LocalVar varname
	;first, we need to know the scope - we can validate the scope at the same time
	!ifdef __FUNCTION__
		!ifdef __UNINSTALL__
			!define LocalVar_Scope_New un.${__FUNCTION__}
		!else
			!define LocalVar_Scope_New ${__FUNCTION__}
		!endif
	!else
		!ifdef __SECTION__
			!ifdef __UNINSTALL__
				!define LocalVar_Scope_New un.${__SECTION__}
			!else
				!define LocalVar_Scope_New ${__SECTION__}
			!endif
		!else
			!error "LocalVar (${varname}) not valid outside function or section."
		!endif
	!endif
	!ifndef LocalVar_Scope
		;first LocalVar for this function
		!define LocalVar_Scope "${LocalVar_Scope_New}"
	!else
		;confirm that the scope is correct
		!if "${LocalVar_Scope}" != "${LocalVar_Scope_New}"
			!error `LocalVar scope changed from "${LocalVar_Scope}" to "${LocalVar_Scope_New}". LocalVarEnd is probably missing from the previous function.`
		!endif
	!endif
	!undef LocalVar_Scope_New
	!ifndef LocalVar_${LocalVar_Scope}
		;this block only compiled for the first local variable in a function/section
		!define LocalVar_${LocalVar_Scope} ;defined now, so won't be compiled for this function/section again
		!tempfile LocalVar_RestoreCode ;this temporary file will be used to write the code that will restore local vars at the end of the function
		;reset the count of local variables used
		!undef LocalVar_Count
		!define LocalVar_Count "0"
	!endif
	;now we can create the local variable that was actually requested
	;increment local variable count
    !define LocalVar_OldCount ${LocalVar_Count}
    !undef  LocalVar_Count
    !define /MATH LocalVar_Count ${LocalVar_OldCount} + 1
	!undef LocalVar_OldCount
	!if ${LocalVar_Count} > ${LocalVar_Max}
		;create the new variable
		Var /GLOBAL LocalVar${LocalVar_Count}
		;update the record of max count
		!undef LocalVar_Max
		!define LocalVar_Max ${LocalVar_Count}
		!undef LocalVar_MaxFunction
		!define LocalVar_MaxFunction "${LocalVar_Scope}"
	!endif
	;link requested define to actual variable
    !ifdef ${varname}
        !error "LocalVar ${varname} already defined."
    !endif
	!define ${varname} $LocalVar${LocalVar_Count}
	;push the existing value of the variable onto the private stack so that it can be restored later
	stack::_dll_insert /NOUNLOAD $LocalVar_Stack $LocalVar${LocalVar_Count} "1"
	;discard the return value
	Exch $R0
	Pop $R0
	;now that we have pushed the value on the stack, we'll need to restore it later.
	!appendfile "${LocalVar_RestoreCode}" `stack::_dll_delete /NOUNLOAD $LocalVar_Stack "1"$\nPop ${DOLLAR}{${varname}}$\nExch $R0$\nPop $R0$\r$\n!undef ${varname}$\n`
	;initialise the variable to blank for this function
	StrCpy $LocalVar${LocalVar_Count} ""
!macroend
 
;restores previous iterations of the function's local vars and undefines the labels
!macro LocalVarEnd
	;the vars that we pushed onto the private stack are in reverse order, so we need to unreverse them
	stack::_dll_reverse_range /NOUNLOAD $LocalVar_Stack 1 ${LocalVar_Count}
	;discard the error code
	Exch $R0
	Pop $R0
	;now we can restore the vars and clean up
	!include ${LocalVar_RestoreCode}
	;final clean up
	!delfile ${LocalVar_RestoreCode}
	!undef LocalVar_RestoreCode
	!undef LocalVar_Scope
!macroend