25 April 2022

Variables and parameters

Let’s discuss some basic issues of variables and parameters and explain auto-complete information about variables and parameters.

This blog post is a sequel to: Where are variables stored?

Variable declaration
Before a variable can be used it must have been declared explicitly. A declaration introduces the variable-name into the compiler’s database. The declaration requires a name for the variable and a datatype so that the compiler knows what to do with that variable; an integer variable is handled completely different than a string variable. Usually, the type is specified in a declaration statement. If the type is omitted the variable gets the default type Variant.

Global i As Int, v      ' an integer and a Variant

Not specifying a type introduces a Variant that might quickly cause confusion and problems. Variant-operations use different functions than – for instance – integer and floating-point operations. When a Variant is used to store a numeric value, simply incrementing it would require the call of a special Variant-Increment function in the runtime. Incrementing a simple data type as Int and Float is an (almost) atomic operation and requires only one CPU or FPU instruction. Therefor, it requires some attention when declaring variables. An error is quickly made as shown in the next line:

Global pic1, pic2 as Picture  ' probably not wanted

This line declares two variables. Two Picture variables are required, but pic1 is a Variant!
Instead use this:

Global Picture pic1, pic2

Before the introduction of auto-complete it was difficult to note these errors. Now auto-complete shows you the type of the variable. Here the type of pic1 variable that was wrongly declared:

Screenshot 2022-03-04 Variant AC

Global, local and static
Other statements to declare variables are Local, Dim, and Static. The Local statement declares a variable that only exists in a procedure. When Dim is used inside a procedure it declares local variables, when it is used in the main part of the program it introduces global variables. (Note that the main part of the program can have local variables as well using the Local statement.) When a procedure returns the local variables go out of scope and the variables are removed from the stack or and the dynamic variables are released (their memory is deleted).
The variables declared with Static are global but only locally visible. They are not freed when they go out of scope, they keep their contents.

Initialization while declaring
Declaring a variable adds it to the compiler’s database, a declaration does not introduce any executable code! A common practice is to collect the declaration of global variables in a separate procedure often called Init or something like that. Note that such a procedure doesn’t need to be executed, i.e. called from the main-part of the program. The procedure would not contain any executable code.
However, this changes if the declaration is used to initialize the variable with a value:

Global s As String = "Hello"

Now the declaration statement contains executable code that needs to executed when the program is run. The statement introduces the variable s into the compiler’s database and produces code to copy “Hello” into the string variable. If the program uses a procedure to declare globals that also initialize the variables, the procedure should be executed when the program is run. The procedure must also be run if the contains array declarations.

A special case is the Static local-variable, which is usually initialized while being declared. The initialization code is executed only once: the first time the Static statement is executed. (This is accomplished by guarding the Static statement by a hidden global boolean variable. After executing the Static statement the hidden boolean is set to true and the statement is never executed again.) Here is an excerpt form gfawinx.lg32’s WinDpi function:

Function WinDpi(hWnd As Handle) As Long

  Static Long pfnGetDpiForWindow = GetProcAddress( _
    GetModuleHandle("user32.dll"), "GetDpiForWindow")

  If pfnGetDpiForWindow       ' works from Windows 8.1
    WinDpi = StdCall(pfnGetDpiForWindow)(hWnd)
  Else
    WinDpi = GetDeviceCaps(Screen.GetDC, LOGPIXELSX)
    Screen.ReleaseDC
  EndIf
EndFunc

The pfnGetDpiForWindow is only initialized once with the function pointer to GetDpiForWindow() API or null if it isn’t supported. If the WinDpi() function is executed again, the pfnGetDpiForWindow variable is still pointing to the API or it is still null. If the API isn’t supported by the Windows version, the DPI of the screen-device context is returned.

Simple datatype parameters
When declaring procedure parameters you need to decide whether to pass a value or variable by value (ByVal) or by reference (ByRef). In general, a parameter is passed by value unless the passed variable needs to be modified. A by value parameter is pushed on the stack by the caller and popped from the stack by the called procedure. Passing a 32-bit integer by value requires 4 bytes of stackspace, passing a Variant by value takes 16 bytes (the size of a Variant).
When a variable is passed by reference the storage location of the variable – a 32-bit memory address -  is pushed on the stack. A Variant passed by reference takes only 4 bytes of stackspace. However, a by reference variable requires an additional step from the compiler: it needs to obtain the address of the variable before pushing it on the stack.

Dynamic datatype parameters
How about passing an array, hash, string, variant, or object (OCX) parameter? Well, an array is simple, it can only be passed by reference. A hash can not be passed without problems due to a bug in GFA-BASIC.

Passing a string by reference is faster than passing it by value. A by value string is first copied in the calling procedure and then the (hidden) copy is passed by reference. It isn’t possible to copy an entire string on the stack! Because the string is first copied, it takes a malloc to allocate the string memory and a memcpy to copy the string’s characters. So, it can be (much) faster to pass a string by reference, you only need to make sure you don’t change the contents of the by reference string parameter. 
Auto-complete cannot differentiate between these types of string parameters and always presents a string parameter with the Ref clause.

Screenshot 2022-03-11 085036

A COM object variable is best passed by value, it only takes 4 bytes to pass the contents of a COM variable. A COM or Ocx variable is a 32-bits integer pointing to the actual COM object. The only need for a by reference COM parameter is when the object must be set to Nothing.

Passing a Variant by value may cause trouble and even a program crash if not handled properly. The rule of thumb is:

Don’t write to a by value Variant parameter (don’t use the by value variant parameter as a convenient extra local variable).

Explanation of variant parameter issue
Often a subroutine parameter is used as an extra local variable that can be written to. For instance, the ByVal s parameter in the procedure foo above can be used to temporarily store a string, s is a copy of the string passed to the procedure. Writing to s won’t affect the string in the caller. A variant containing a string that is to be passed to a procedure by value does not copy the string before invoking the procedure.
Dim vnt = "Hello"
foo(vnt)        ' by value
Trace vnt       ' wrongly displays Hello

Proc foo(ByVal v As Variant)
  v = "GFA BASIC GFA BASIC GFA BASIC"

This code sample produces problems. The vnt variable stores a pointer to an OLE string containing “Hello”. When passed by value the parameter v is a copy of vnt, a 16 bytes data-structure with type information (VT_BSTR) and a pointer to the OLE string “Hello” on the stack. Assigning a new string to v will release the OLE memory currently pointed to by v. The new OLE string’s memory address is stored in v, together with the new data type (again a VT_BSTR). When leaving a procedure parameters aren’t cleared, so the foo procedure does not free the new contents of v. The variant’s 16 bytes occupying the stack are simply popped off the stack, leaving the new OLE string unreferenced. After returning from calling foo there is nothing that holds a pointer to the new OLE string and the OLE memory will never be released, the program is leaking memory.

Now, why does Trace vnt display “Hello”? After executing foo, the vnt variable is still referencing the OLE memory allocated by the assignment of “Hello”. The OLE string was released in foo when the new string was assigned, but the original variable vnt is never updated. The variable vnt still references the memory bytes where Hello was stored, bytes that weren’t actually cleared when released. The variable vnt references released OLE memory. When vnt goes out of scope, at the end of the program, it is released by GFA-BASIC by calling the OLE system function VariantClear(). Since the variable vnt points to released memory, the program may crash.

The type of procedures and parameter-defaults
To declare a subroutine you can choose between a Procedure, Sub, Function, or FunctionVar. The rule of thumb here is to use a Procedure or Function, unless you explicitly need a Sub or FunctionVar. The Sub is needed for event procedures where the parameters are passed by reference, the default behavior for a Sub. However, using a Sub as a general procedure might cause problems due to a flaw in the default by reference behavior. See the link at the end of this post for more information. If you use a Sub for something else than event subs make sure to use an explicit ByRef or ByVal clause in the declaration of the parameters.

Default behavior of procedures and functions:

Type of subroutine  Default ByVal or ByRef  Default datatype  Default return datatype
Procedure ByVal Double -
Sub ByRef in event subs,
otherwise flawed
Variant -
Function ByVal Double Ref Variant
FunctionVar ByRef Variant Ref Variant

As you can see, a function’s default return type is a by reference Variant. This means that the caller of the function passes a Variant by reference, the variant is ‘owned’ by the caller. This is illustrated by the following example:

Proc foo()
  Dim v As Variant
  v = GetValue()        ' passes v by reference
EndProc
Function GetValue()     ' default is variant
  GetValue = 10         ' this references v from foo
EndFunc

The GetValue = 10 assignment writes to the v variable from foo() directly.

Here you see the auto-complete information of the function-variable GetValue:

Screenshot 2022-03-06 101409

If the function’s return type is String, Object (or other Ocx type) or a user defined type, the caller passes a by reference variable to the called function.
A function cannot return an array or Hash datatype.
Note - Each function automatically gets a ‘local’ variable with the function’s name and type. This function-variable can only be used to assign a value, it is not available as a real local variable that can be used to read from; consequently auto-complete won’t show the function variable in a ‘read-context’.

Finally
Pay attention when declaring variables to not introduce unwanted Variants. Explicitly use ByVal or ByRef when declaring subroutine parameters, and also explicitly specify the datatype (either by name or by postfix). Auto-complete always shows by reference for string parameters, even if they are passed by value. The default return type of a function is a by reference Variant. Don’t use a Variant parameter as a general local variable, ie. don’t write to the variant.

See also: Function and Sub parameters & Passing default ByRef parameters to a Sub