Chapter

5

Explaining InstallScript

fter developing the requirements for the install program and validating A those requirements in a specification and prototype, you’re ready to start coding. Efficiently translating the specification into code reduces the number of bugs and minimizes the amount of time you’ll spend at the keyboard. Efficient translation comes from understanding InstallScript and the requirements of your install program. Once you understand InstallScript, you can use the language to your advantage. Unfortunately, understanding InstallShield takes time and knowing how to use it requires practice. The goal of this chapter is to describe InstallScript—its data types, constructs, general practices, and behaviors. I hope that I’ll also be able to pass on some useful information that will shorten the path to effectively using InstallScript. For additional information, refer to the first few chapters in InstallShield’s Language Reference Guide.

5.1 InstallScript Notation If you don’t have prior experience in programming, the last chapter may have made an install program seem like a collection of meaningless words. Unless you know what those words mean, it’s difficult to decipher the flow of the pro-

153

5 • Explaining InstallScript

154

gram. Each of the words and their placement play an important role in the success of your install program. As we’ll see, an install program is a logical sequence of words, phrases, white space, and formatting. This sequence implements your install specification in a readable and maintainable form. It’s a precise and exacting form of expression that’s called InstallScript notation. The syntax of InstallScript is a hybrid of C and Pascal. InstallScript, like most other development languages, consists of four distinct parts: variables, predefined constants, InstallShield keywords, and the InstallShield built-in functions. The built-in functions are referred to collectively as the Application Programmer’s Interface or API.

5.1.1 Variables Like many other things in programming, a variable is a convenience. Clearly, it’s not too convenient until you know what it represents. Let me give you an example. I live in a house, which is located on a street in Marlborough, Massachusetts. I call it “my home.” Although the address of the house is the more exact representation of its location, I refer to my house as “my home” because it’s convenient. The phrase “my home” can be considered a variable that represents the postal address of where I live. If I were to move from Massachusetts to California, I could still use the phrase “my home” and it would still represent my house. This time the postal address that “my home” represents would be the street address of the new house in California. This example demonstrates two of the most common reasons to use variables. First, to refer to something that can be conveniently represented by something else—usually in a shorter or more descriptive fashion. Second, to refer to something that has the potential to change. As we saw, while the name of the variable “my home” remained constant, what it represented changed.

5.1.2 Data Types While a variable can be extremely useful to us humans, it holds little value for a computer program. In fact, between the compiler and the running program, variables are inconvenient. When the compiler processes our script, it implements special handling of variables and it’s up to the running program to keep track of the changes so the correct value is always used. Because this is such a big task, the compiler requires us to do some preliminary bookkeeping. That is, we must tell the compiler what type of data the variable represents.

In InstallScript, there are nine different data types; however, there are really only three different types of data. The three basic types of data are numbers, characters, and memory addresses. The reason that there are more that three data types is because there can be different types of numbers, different types of characters, and different types of memory addresses. The differences are denoted by data types. For numbers, InstallScript provides three data types: LONG, SHORT, and BOOL. A LONG data type can represent numbers within the range of –2,147,483,648 to 2,147,483,647. On the other hand, sometimes the numbers you’re referring to might not be that large. In those cases, you may want to use a SHORT data type. A SHORT represents numbers within the range of –32,768 to 32,767. A BOOL doesn’t usually contain large values; instead, the BOOL data type maintains one of two states: TRUE or FALSE. A TRUE value is indicated by a value of 1, while FALSE is usually indicated by a nonzero value. Below is an example that illustrates the declaration of numerical data types. Don’t be too concerned about my choice of variable names; we’ll get to that in a minute or two. LONG lExpectedValue SHORT shSelection BOOL bError

// declaration of a 4-byte number // declaration of a 2-byte number // declaration of a Boolean

An important consideration when determining the assignment of data types is how many bytes the operating system reserves for the variable. For LONG data types, the operating system reserves 4 bytes. For SHORT data types, the operating system reserves 2. This becomes important when we begin using the Windows API to augment the InstallShield API. In that case, the exchange between data types must be consistent. In earlier versions of InstallShield, there were two additional data types: NUMBER and INT. A NUMBER data type was similar to a LONG, while the

155 5.1 InstallScript Notation

Just as “my home” refers to a physical construction called a house, I could also refer to “my car” as a convenient expression for the make and model of the car I’m driving. When I’m home, “my car” refers to a Saab. When I’m on the road, “my car” refers to the vehicle the rental company had available (and I can afford). If I were talking to you about my home and referred to it as my car, you would give me a quizzical expression and wonder what I was talking about. This is a rough approximation about how data types are used. It’s the association of a meaningful construct with a variable. When you and I are talking about our homes, we generally know what each other is talking about without having seen the respective houses.

5 • Explaining InstallScript

156

INT data type was similar to a SHORT. That is, similar as long as you’re running on a 32-bit operating system. On a 16-bit operating system, the byte width of these data types varies. For this reason, you should use the LONG and SHORT whenever possible. This way you’re assured of the appropriate byte width independent of the operating system. Before we move on, there’s one more thing about the numerical data types that should be mentioned. InstallScript doesn’t provide any unsigned number data types. This becomes an issue if you’re attempting to construct a Windows API parameter list within InstallShield data types. Because of the lack of unsigned data types, you’ll have to use a LONG or a SHORT data representation. When using InstallShield number data types as a representation for unsigned numerical values, a returned negative value means that the number is actually a large signed value. You’ll have to make some adjustments to calculate the real value. For character and string support, InstallScript provides two data types: CHAR and STRING. A CHAR contains a single ANSI character and is a single byte wide. While internally InstallShield implements the STRING data type as a collection of CHARS, it’s actually a memory address or pointer to a collection of CHARs. Below is an example that illustrates declaring string data types. STRING STRING STRING CHAR

svUserName; // autosized string svPath[ _MAX_LENGTH ];// explicitly- sized string szDir[25]; chIndex;// declaration of a single character

When you declare a variable of type STRING, you can either specify its default length or let InstallShield autosize the string length. In most cases, it’s easiest to let InstallShield take care of the initialization. In the sample declaration above, the svUserName string variable is autosized by InstallShield. When autosizing, InstallShield initially allocates half as much buffer space as the maximum it supports and then monitors the string. When the string requires additional space, InstallShield bumps the string length up to the maximum value. This all happens transparently; normally you wouldn’t even be aware that it was happening. For 32-bit operating systems, InstallShield initially autosizes STRING variables to 512 characters; for 16-bit operating systems, the autosize length is initially 256 characters. When the use of the string requires more than these initial amounts, InstallShield transparently doubles the string buffer size. For 32-bit operating systems, the maximum buffer size is 1024 characters; for 16-bit operating systems it’s 512 characters. The important point is that autosizing doesn’t

Because a string buffer can be allocated for much more memory space than it uses, there must be some way to determine the end of the string. In the example above, svUserName is initially allocated to 512 bytes (or characters) in a 32-bit operating system. This is probably more than we’d need and when we use svUserName we’d rather not see all the empty or unused characters that appear after the usable data. To this end, InstallScript inserts a NULL character after the last usable character. A NULL character is the ASCII value “00.” However, within InstallShield this is denoted as “\0,” where the backslash is an escape character. The escape character is an internal device used by InstallScript and other languages to denote a secondary use of a normal character. An example of the relationship between a CHAR and STRING is using a string index to identify a particular character. Take a look at the following example. STRING CHAR

szString; chChar;

szString = "I learn to type by typing."; chChar = szString[2]; // chChar is ‘l’ or \108

Bear in mind that strings are zero-indexed. That is, the first character is located at position zero. In this example we used an index of “2” to retrieve the third character. That character is “l” or the ASCII value 108. One more thing about strings—if you do explicitly specify the string length, you must maintain the proper length for all subsequent values. This is particularly important when using a string whose content is determined by appending multiple strings or can be altered by the user. Attempting to fill a string beyond its buffer size causes a run-time error. Now let’s talk about memory addresses and how they relate to data types. Associated with every variable is a memory address. That’s because the operating system and the application need to know how to retrieve those variables. They know where to retrieve the contents of a variable because of the associated memory address. They know how many bytes to retrieve because of the specified data type. Another expression for a memory address is the term pointer. A pointer is the memory address of a variable. For some memory structures, it’s easier to refer to the variable by its memory address or pointer value. If you’ve followed

157 5.1 InstallScript Notation

immediately provide the maximum buffer length. If this is what you require, then you must explicitly initialize the variable with the _MAX_LENGTH keyword. Just as an aside, string lengths can be much larger than this value; however, this isn’t recommended and you will get a compiler warning.

5 • Explaining InstallScript

158

me so far, than it should make sense when I say a pointer is not useful by itself. That’s because it’s only a reference to a data type and you need to know what that data type is for the pointer to make sense. In brief, a pointer can be thought of as an address data type—it’s another way to look at the same data. A pointer is a data type that contains the memory address of a variable. Let’s look at an example. Each of the variables used in this example is stored in system memory. Along with each variable is an address in system memory. Those addresses are the pointers to variables. This is illustrated in Figure 5–1. Address

Value

0x034A121E

12

0x034A1222

531

...

...

...

...

0x034A122A

"The ..."

...

...

... if ( lValueOne != lValueTwo ) then AskYesNo( szString, YES ); endif; ...

Figure 5–1 This figure illustrates a value and its associated memory address. The

snippet of code uses two LONG variables and a string variable. The memory address associated with each variable is the address of the first byte of the data. What isn’t shown in this figure is how many bytes of memory are contained with each value.

So where does this leave us? Well, we can now talk about the remaining data types: HWND, LIST, and POINTER. Each of these data types refers to an address in memory. An HWND or handle contains the address of a Windows construct, for example, a dialog, a window, a control, a font, and so on. Within the Windows API, there are many more types of handles and they are all derivatives of the HWND type. A LIST is the address of a “chunk” of memory. A list can be thought of as an array of string or numerical values, that is, a collection of a group of strings or numbers. As items are added or removed from the LIST, memory is requested or returned. Before we move on, I’d like to spend a few minutes explaining how to use a POINTER data type. Because of its unique nature, a pointer can be assigned

POINTER POINTER STRING LONG

plNumber; pszString; szString; lNumber;

lNumber = 45; szString = "I learn to develop by developing."; plNumber = &lNumber; pszString = &szString; . . . lNumber = *plNumber; szString = *pszString;// NOT ALLOWED!

The asterisk is the dereference operator, because a pointer is known as a reference to a data type. To look at what data type the pointer contains you need to dereference the pointer. This example also demonstrates a shortcoming in InstallScript—you cannot dereference a pointer to a STRING. But this is redundant, because a STRING data type is already a POINTER. What’s actually odd is that you can assign the address of a string to a pointer. The last data type that we’re going to look at is a special one. It’s one you create yourself and it’s called a structure. A structure is a convenient way of grouping data together. It’s a construct that’s intended to make your life easier. Instead of remembering a variety of variable names you can group them together under a single data type. A structure has the form shown here. typedef ; ; ; . . . end;

The structure definition uses the keywords “typedef,” “begin,” and “end.” The name you assign with the typedef keyword becomes the name of the structure data type. The structure becomes a new, valid data type.

159 5.1 InstallScript Notation

the address of any data type. To do this, we’ll use the “&” or address operator. To take a look at what the pointer is referencing, we’ll use the “*” or dereference operator. This process is illustrated in the code fragments below.

5 • Explaining InstallScript

160

Once you’ve assembled a valid structure, you need to assign the structure to a variable name. To get to the structure members, you’ll use the “.” member operator. The code sample below illustrates how this works. typedef NUMBERGROUP LONG lNumberOne; LONG lNumberTwo; LONG lNumberThree; end; NUMBERGROUP ngrpNumber; program ngrpNumber.lNumberOne = 23; ngrpNumber.lNumberTwo = 45; ngrpNumber.lNumberThree = 89;

With a large install program, a structure is a convenient way of keeping similar data together. For example, you could use a structure to maintain the user’s selections from Wizard page to Wizard page. The real power of a structure becomes apparent when used with pointers. With a pointer to a structure, the structure can be passed into a function as a parameter. In fact, passing a structure into a function requires that you use a pointer to the structure. Which brings up an excellent use for a structure—to reduce the number of parameters passed into a function. When using a pointer to a structure, you’ll need to use the “->” or structure pointer operator to access the internal data members. Keep in mind that structures cannot directly appear as parameters to a function. The only way to pass a structure into a function is through a pointer. Let’s take the same structure declaration as the previous code example. Only this time, we’ll initialize the structure from the pointer. NUMBERGROUP ngrpNumber; NUMBERGROUP POINTER pngrpNumber; program pngrpNumber->lNumberOne = 23; pngrpNumber->lNumberTwo = 45; pngrpNumber->lNumberThree = 89; . . .

There is a significant difference when declaring a pointer to a structure. You’ll need to precede the POINTER keyword with the name of the structure. For standard data types, a pointer data type could point to any numerical or

Table 5–1 InstallShield Data Types Data Type

Type of Data

BOOL

number

CHAR

ANSI character

HWND

Number of Bytes

Description

16-bit: 2 32-bit: 4

A BOOL maintains one of two states: TRUE or FALSE. TRUE is indicated by a value of one, while FALSE is usually indicated by a nonzero value.

1

A CHAR holds a single ASCII character as a byte-sized, numerical value.

address

16-bit: 2 32-bit: 4

An HWND holds a system pointer to a Windows construct.

LIST

address

4

A LIST is a pointer to a structure in memory. LIST structures must be defined as STRINGLIST or NUMBERLIST.

LONG

number

4

A LONG maintains a number value within the range of –2,147,483,648 to 2, 147,483,647.

POINTER address

4

A POINTER maintains an address in memory.

SHORT

number

2

A SHORT maintains a number value within the range of –32,768 to 32,767.

STRING

address

4

A STRING maintains an array of CHAR variables. In InstallScript, the contents of a STRING variable are appended with a NULL character (“\0”). The maximum 16-bit size is 512 characters. The maximum 32-bit size is 1024 characters.

address

4

A structure is a user-defined data type that is comprised of individual InstallShield data members.

161 5.1 InstallScript Notation

string data type. For structures, a pointer data type can only reference the specified structure data type. We’ve covered a lot of ground in this section. Much of the information is summarized in Table 5–1. You may find it useful as a quick reference.

5 • Explaining InstallScript

162

5.2 Naming Variables Choosing the name for a variable requires some forethought. Ideally, the contents of a variable should be immediately obvious from its name. A descriptive meaningful name improves readability and makes code maintenance easier. Taking a step in the right direction is prefixing symbolic characters that identify the data type. As shown in Figure 5–2, there are three potential prefix locations that are often used in naming variables. VariableName g

b c h l list p s sh

z v

Figure 5–2 This figure illustrates variable naming. After creating a meaningful name,

the next step is assigning prefix characters. The characters that precede the variable name provide immediate association of the variable with its data type and intended use.

In the first location is the prefix that often precedes all others. If present, the “g” indicates that the variable has global scope. Variables defined at the beginning of setup.rul are automatically global and, for enhanced readability, should be prefixed with the “g.” A global variable is one that can be read or written by any function that is included in setup.rul. The “g” prefix distinguishes these variables from those that are local to a function. In the second location is the prefix for data type. As you can see from Table 5–2, these prefixes are easy to remember.

Prefix

Description

b

A Boolean variable

c

An ASCII character variable

h

A handle variable

l

A 4-byte numerical variable

list

A 4-byte reference to a LIST structure

p

A 4-byte reference to a memory address

sh

A 2-byte numerical variable

s

A 4-byte reference to a string variable

The last prefix location indicates if the name represents a variable, constant, or literal. A “z” in this position indicates that the associated variable is a string constant, a variable, or a literal. On the other hand, a “v” indicates that the associated variable is a string constant or literal. My personal preference is to use a “v” on variables that I don’t want to alter once I’ve defined them; on the other hand, a “z” variable is one that I can alter without worrying about its impact elsewhere. Keep in mind that the naming of variables is only as good as its implementation. It’s up to you to name variables and ensure that they are used correctly. The work is worthwhile. Taking the effort to prefix and creatively name a variable has long-term benefits. It allows readers of your code to immediately distinguish between variable types and the roles of those variables. This increases your code’s readability, leads to easier code maintenance, and shortens debug time. As an aside, while the InstallShield documentation refers to this naming convention as Hungarian notation, that’s not the case. True Hungarian notation uses prefix characters to specify the role of the variable not the data type. In fact, the intention of Hungarian notation is to name variables so that they are operating system portable. Using prefixes that are based on data types locks the variable to a specific “byteness.” An example of Hungarian notation is specifying a variable that contains a summation of numbers with a prefix “sum” or counter variable with the prefix “count.” For the remainder of this book, however, we’ll adopt InstallShield’s data type prefixes. Your best choice is to adopt the standard that’s used by the developers in your organization. This will help you make the transition into the application development environment and make your code more readable by peer

163 5.2 Naming Variables

Table 5–2 Hungarian Notation Prefixes

5 • Explaining InstallScript

164

developers. In the event that there is no adopted standard, the best course of action is to follow InstallShield’s practice. After all any coding standard is better than none.

5.3 Programming Constructs As amazing as it may seem, all the code that you write comes down to a handful of programming constructs. A construct is phrasing one or more words that work together for a common objective. Much as the words in a sentence combine to invoke a mental expression, a programming construct forms a logical expression or intent. InstallShield supports the standard programming constructs: abort goto return if...then...else...endif

switch . . . endswitch for . . . endfor repeat . . . until while . . . endwhile

5.3.1 Abort The abort keyword causes the immediate termination of the install program. Typically, an abort is the abnormal completion of the install program. When an abort line is executed, the install program immediately stops and the cleanup utility (_isdel.exe) removes any InstallShield-specific temporary files and directories. In addition, the uninstall engine backs out any files that were copied or changes that were made to the user’s machine. Earlier versions of InstallShield used the exit keyword—it’s recommended that the “endprogram” keyword be used instead. An install program may contain any number of aborts, but only a single endprogram. The endprogram represents the normal completion of an install program.

5.3.2 Goto The goto keyword initiates an immediate nonsequential jump in the logical flow of the program. A goto statement is used in coordination with a label. After all, if we’re going to tell the program to take a jump, it’s only appropriate to say where. Because the point of execution jumps out of the normal processing, the goto statement is considered a poor programming technique. As in most other languages, InstallScript provides suitable alternatives. With that said, the goto statement does work well with the display of Wizard pages. Because each Wizard page has a Back and Next button, the page

The code generated by the Project Wizard uses the goto statement within the main flow of the program. In this case, the goto yields compact, yet readable, code. A code fragment is shown here. if if if if if if

(ShowDialogs()