April 2008 - Posts

Part Four - A simple C# app to modify an ISM

In the last post on this topic we defined an attribute for a <script> task. Note: You can find the file, as we last left it, attached to the previous post if you want to go grab a copy of it for reference. The finished file for this post is attached at the bottom of this post.

ImportStrings parameters

The ImportStrings method has 4 parameters, so let's get each of these declared and configured within the ExecuteTask function code.

1. The path to the file containing the strings to import - a string

To create a file containing strings to import just open a plain text file. At the beginning of each line write the string ID, put a tab, then put the string. Do not surround the string in quotes. Make sure to use a tab and not a space. The string must all be on the same line in the file. The end of the file should be at the end of the last line. If there is an extra CRLF in the file ImportStrings won't like it.

We've already declared a task attribute to hold this value: importfile.
And in our ExecuteTask function, the importfile attribute's value gets placed into the _importfile string that we've declared.

2. The langauge ID of the particular string table (e.g. are these English, French or Italian strings?) - a string

If you only have an English string table in your project, then the language ID will be "1033". If you open the ISM in the IDE and look in the Direct Editor at the ISString table, the ISLanguage_ column is the langauge ID. Remember that this value is a string even though the content is all digits.

I didn't create a task attribute for this value because I'm always using the English string table. However you could easily declare another task attribute just the same way you did for the importfile.
Instead, in the ExecuteTask function I declared and set the _langaugeID string to "1033" right from the beginning.

3. Whether or not to overwrite existing strings of the same ID - a reference to an object

Now things get a little weird. There is a special enumerator defined just for this ieoverwrite value. If you were in Visual Studio you could see this enumerator by the title "SAAuto12.ImportStringTypes". The two possible values are eiIgnore or eioverwrite. One means to overwrite strings with the same ID, and the other means to ignore duplicate string IDs and skip them.

The task attribute for this value is declared as a string: overwrite.
I also declared a string in the ExecuteTask function called _overwrite.

The problem is that somehow this string needs to get converted to an object reference for the specific ImportStringTypes object. I tried several ways of doing this (including trying to declare the task attribute itself as an object instead of a string) and wasn't able to find a simpler method. So this may be a bit of a kluge, but it works.

In the ExecuteTask declare an object of the ImportStringTypes type and set it to a default value of overwrite. I'd do this before the project.OpenProject method is called.

object _eiType = ImportStringTypes.eioverwrite;

Now make a switch block that takes the string "true" or "false" value for the _overwrite variable and converts it to the corresponding enum value. Make the default value = overwrite.

switch (_overwrite)
{
  case "true":
    _eiType = ImportStringTypes.eioverwrite;
    break;
  case "false":
    _eiType = ImportStringTypes.eiIgnore;
    break;
  default:
    _eiType = ImportStringTypes.eioverwrite;
    break;
}

When we actually use this variable in the ImportStrings method, we will use it as "ref _overwrite" so that we get a reference to it, as required.

Note: If you look at the IS help for this method it says that the overwrite parameter is optional. This works when I use a simple VBscript to run it. Unfortunately, I tried leaving this parameter off the ImportStrings function call when calling it from this C# code and it always failed with a message that the parameter was required. So... I always provide it. *shrug*

4. The path to write out the log file - a reference to an object

This is simply the path to which you want a log file created, along with the log file name. However this parameter has some of the same funkiness as the overwrite does -- it needs to be an object.

The task attribute is already declared as a string: logfile
The ExecuteTask variable is also a string as: _log

Again, to get our conversion to an object, declare an object inside the ExecuteTask method and set it equal to the _log string. I'd do this before the project.OpenProject method is called.

object _logfile = _log;

When we actally use this variable in the ImportStrings method we will use it as "ref _logfile" so that we get a reference to it, as required.

Note: If you look at the IS help for this method it says that the log file parameter is optional. Just as with the overwrite parameter, I got errors unless I provided it.

 

Basic Error Handling

Before we attempt to open the project, let's cover a few error conditions and handle them.

Let me state right now that I've been told this is not 'proper' C# coding - it's not best practice to use try/catch blocks in this way if you are doing full on C# classes and whatnot. Alright, so be it. I'll clean it up and make it pretty later :-) But for now, it works and seems adequate for this usage. So here we go.

Right after declaring our project object, start a try block. Inside that block we will test for a few different error conditions. Any error will throw us into the catch block where we log an error to nAnt providing it with the specific error condition, then issue a single task-level nAnt task error to indicate that our custom task has "failed".

ISWiProject project = new ISWiProject();
try
{
  ...set of error handlers described below goes here...
  project.SaveProject();
  project.CloseProject();
}
catch (Exception ex)
{
  project.CloseProject();
  Log(Level.Error, "ERROR: Cannot import strings. {0}", ex.Message);
  throw new BuildException("Cannot import strings");
}

 

In these error checks I've used the String.Format method in order to be able to insert the relevant variable's value into the error message. You'll see that String.Format lets you give a string, then put placeholders in for each of the items you want to insert into the message, such as {0} {1} {2}. Then just give each of the items to replace separated by commas.

1. Error: The import file doesn't exist

Make sure that the file exists with a simple call to the System.IO.File.Exists method. If it fails, throw an error.

if ( !System.IO.File.Exists(_importfile) )
{ throw new Exception(String.Format("Cannot locate import file {0}." , _importfile )); }

2. Error: The installer project file doesn't exist

Make sure that the ISM file exists, same as with the import file.

if ( !System.IO.File.Exists(_ismpath) )
{ throw new Exception(String.Format("Cannot locate ISM {0}." , _ismpath)); }

3. Error: The installer project exists, but it is locked or otherwise unable to be written to

In this case the OpenProject method will not return an error, so unless we check for it we won't realize that the project is successfully opened, but can't be written to. Open the project and check the return value. Any value other than zero is an error for us.

if ( project.OpenProject(_ismpath, false) != 0)
{ throw new Exception(String.Format("Failed to open ISM {0}." , _ismpath)); }

4. Error: The ImportStrings method fails in some way

We actually call the ImportStrings method now, but check to make sure it worked. We use the 4 parameters that we defined and configured above.

if ( !project.ImportStrings(_importfile, languageID, ref _eiType, ref _logfile) )
{ throw new Exception("Cannot perform import"); }

 

And that's all she wrote!

This should be a functioning custom task for importing strings. Hope this little exercise is useful to you in some way.

Tip: Editing nAnt files in Visual Studio

I used to think that opening nAnt files in Visual Studio was a lot of wasted overhead and not worth it. But I've realized that the auto-completion and IntelliSense features make it very worthwhile... so... here is how you setup Visual Studio 2005 to edit nAnt files. 

  1. Go to the directory where you installed nAnt. In the schema subdirectory is nant.xsd.
  2. Copy this file into the Visual Studio 8\XML\Schemas folder.
  3. Open Visual Studio. Open a project.
  4. From the menu Project > Add New Item…
  5. Select the template for an XML File. Give the file a .build extension.
  6. If the new file isn't already opened, go to the Solution Explorer and open it.
  7. Click somewhere in the opened .build file.
  8. Look at the pane that shows the properties for the file. One of the properties is Schemas. Click the … browse button and locate the nant.xsd file and check the box next to it.

Now you will be able to use the IntelliSense features of Visual Studio to see schema errors, syntax errors, and get auto-completion of your elements.

Special note... there is a bit of odd behavior that you might notice.  I discovered that if I clicked on the .build file in the Solution Explorer, the properties tab would change and I would no longer see the Schemas field, BUT if you then click inside the editing window for the .build file, the Schemas field reappears. Just some weird behavior from VS... Wink

Posted by SusanGorman with no comments
Filed under: ,

Part Three - A simple C# app to modify an ISM

Note: The finished file is attached to this post. Scroll down to the bottom of the post for the link. 

In my previous post I wrote a simple C# <script> task that could open an InstallShield ISM from an nAnt script. The eventual goal is to call the Import Strings function to import a string table into the ISM. I typically use a function like this when I have a master installer project from which I can build 2 different branded versions of the same product. I create a string table text file that contains only the strings that are different between one brand an another, then import the correct version of the string table depending on which brand I'm building. To do that we are going to need to have a few pieces of data that we can pass to the custom task whenever we call it.

Where we last left our hero<ic> nAnt script it could open an ISM project but didn't actually modify it. Here is what it looked like:

<target name="customtask">
  <script language="C#">
    <references>
      <include name="c:\prgram files\nant\bin\interop.isappserviceslib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.ismautolib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.ismupdaterlib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.isupgradelib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.iswibuildlib.dll"/>
      <include name="c:\prgram files\nant\bin\interop.saauto12.dll"/>
      <include name="c:\prgram files\nant\bin\interop.vba.dll"/>
    </references>
    <imports>
      <import namespace="SAAuto12"/>
    </imports>
    <code>
      <![CDATA[
        [TaskName("modifyISM")]
        public class modifyISM : Task
        {
          protected override void ExecuteTask()
          {
            ISWiProject project = new ISWiProject();
            project.OpenProject(@"c:\projects\myism.ism",false);
            project.SaveProject();
            project.CloseProject();
          }
        }
      ]]>
    </code>
  </script>
</target>
<target name="runCustomTask">
  <modifyISM/>
</target>

First, let's create an attribute for our task which we can set to the path to the ISM. Locate the line between the last curly brace and the ending square brackets. Insert a new line at this point and insert a declaration for an attribute like this:

}
[TaskAttribute("ismpath",Required=true)]
public string ISMPath
{ get {return _ismpath;} set {_ismpath = value;} }
]]>

The instead of "ismpath" you can use whatever you want the attribute to be called. And obviously you can set Required to true or false. I defined this attribute as a string. The difference between _ismpath and ISMPath is that ISMPath is the publicly visible string name, wherease _ismpath is only visible internally. This little set of code exists to allow the nAnt script to 'set' the internal variable to whatever value is passed in via the attribute. It ensures that the value of _ismpath only changes in a 'safe' way, by OUR function. This would be much more relevant in a larger task or code that was actually going to get called by other code. For our simple code it isn't really critical, but it's good practice to get used to the idea.

Now modify the line where we used to have a hard-coded path and replace it with this variable:

project.OpenProject(_ismpath,false);

In addition, modify the target from which we actually call the custom task and provide the attribute for ismpath:

<modifyISM>
  ismpath="c:\projects\myism.ism"
</modifyISM>

Try this out now. You could even go a step further and create a property to hold this path so that you could provide the ismpath on the nAnt command line for ultimate flexibility!

To prepare us for the next phase of this custom task, let's go ahead and declare a few more attributes that we are going to need: the path to a string table text file to import, the desired location for the import strings log, and an optional flag saying whether to overwrite existing strings of the same name. Insert these lines declaring the new internal variables right ABOVE the line starting the ExecuteTask():

private string _ismpath;
private string _importfile;
private string _log;
private string _overwrite; 

Then, go down to the first TaskAttribute add these lines immediately BELOW the TaskAttribute:

[TaskAttribute("importfile",Required=true)]
public string ImportFile
{ get {return _importfile;} set {_importfile= value;} }
[TaskAttribute("logfile",Required=true)]
public string LogFile
{ get {return _logfile;} set {_logfile= value;} }
[TaskAttribute("overwrite",Required=false)]
public string Overwrite
{ get {return _overwrite;} set {_overwrite= value;} }

Go ahead and add these attributes to the <modifyISM> call in the runCustomTask target.

<modifyISM
  ismpath="${ISM}"
  importfile="c:\projects\myStringTable.txt"
  logfile="c:\projects\import.log"
  overwrite="true"
/>

There you go. You have the framework in place. Next time we'll tackle some error handling and actually making the call to ImportStrings.

Part Two - A simple C# app to modify an ISM

So in my previous post I got a simple C# app written that modifies the ISM.

Alright, now to put this into the nAnt script. I'm going to assume a basic familiarity with nAnt here...  Note: I'm using nAnt .085 build.

Of course, start with a <target> that will contain the new task definintion. Inside the target start a <script> task. The language we are writing in is C#.

<target name="customtask">
<script language="C#" >

Now the first thing we must do is provide all the references so that our code can interact with the InstallShield Standalone Automation. These are going to be all the same references that we saw added in Visual Studio. For my task this is the list:

<references>
    <include name="C:\program files\nant\bin\interop.isappserviceslib.dll" />
    <include name="C:\program files\nant\bin\interop.ismautolib.dll" />
    <include name="C:\program files\nant\bin\interop.ismmupdaterlib.dll" />
    <include name="C:\program files\nant\bin\interop.isupgradelib.dll" />
    <include name="C:\program files\nant\bin\interop.iswibuildlib.dll" />
    <include name="C:\program files\nant\bin\interop.saauto12.dll" />
    <include name="C:\program files\nant\bin\interop.vba.dll" />
</references>

When we wrote the C# console application in the previous blog, the app worked because it created all the above DLLs to allow the C# managed code to be able to talk with the COM interface. These dlls were all on disk and accessible to your C# console app when it ran (as I understand it). So we need to accomplish the same thing for our C# nAnt task -- it needs to have access to all these same DLLs. In my case, I copied these DLLs to my nAnt program directory so that they would be available anytime this nAnt script runs. You might want to put yours in a different location -- as long as they are reliably going to be in that location when your script runs.

The next item is the <imports> section. The imports section corresponds to the "using" statement that we added in the original source code. So again, it is optional, but adding it allows us to shorten our code a bit since we can leave off the "SAAuto12" each time.

<imports>
    <import namespace="SAAuto12"/>
</imports>
 

Now we get to the code itself: The <code> section. I'm sure somebody could explain why, but all I know is that your code section will all be inserted between some sort of CDATA and brackets characters, like this:

<code>
    <![CDATA[
--- your code goes here ---
    ]] >
</code>

Again, I haven't looked up the meaning of this, I just know it works. Confused

In order to run the code as an nAnt task, we need to format it as a task. That requires a few extra sections of code from what we wrote previously. As a reminder, the original code was this:

    ISWiProject project = new ISWiProject();
    project.OpenProject(@"C:\projects\myism.ism",false);
    project.SaveProject();
    project.CloseProject();

Our new code starts by declaring this as a new type of Task (a class we inherit from nAnt)and implementing the "ExecuteTask" method in this class. It looks like this:

    <code>
    <![CDATA[

    [TaskName("modifyISM")]
        public class modifyISM : Task
        {
            protected override void ExecuteTask()
            {
                ISWiProject project = new ISWiProject();
                project.OpenProject(@"C:\projects\myism.ism",false);
                project.SaveProject();
                project.CloseProject();
            }
        }

    ]] >
    </code>

Now.. close up your <script> and <target> sections and you have finished coding this new custom nAnt task!

    </script>
    </target>

Let's do a quick check and make sure our new task 'customtask' can be run without error. Now when you run the 'customtask' target itself, all you are doing is compiling the C# code -- it does NOT actually execute it. 

Go to a command prompt and run your nAnt script calling the customtask target. For example I'd go to the dir where my nAnt script is located and run: nant customtask.

You should see a portion of the output that looks like this:

customtask:
     [script] Scanning assembly "cytjfj4d" for extensions.

BUILD SUCCEEDED

This is good! This means that it was able to successfully compile the code and create an assembly out of it. The assembly name is a randomly generated name and will be different every time. If you get an error -- scroll up through the build output and locate the first error. It should give you some sort of hint as to what needs fixing -- and the error should be very similar to what you would see if you were compiling this in the Visual Studio IDE.

Now that we know it compiles -- let's get wild and add an actual call to execute that code. Devil

<target name="runCustomTask" >
    <modifyISM/>
</target>

Run your nAnt script again, this time calling the new target after calling the customtask. Such as, nant customtask runCustomTask.

If all has gone well your nAnt script will complete without error and you get BUILD SUCCEEDED!

Next post I'll tackle defining parameters that you want to pass to your custom task and making this custom task actually modify the ISM. The example will be calling the ImportStrings function.

Part One - A simple C# app to modify an ISM

As part of my nAnt build process, I discovered I needed to modify a setting in my installer at build time.

InstallShield's Standalone Automation Interface provides a pretty simple way to interact with the install project using COM.
 
Note: I use the standalone version of the automation interface because it doesn't require having the full IDE installed on the box. The 'normal' automation interface can be used to perform the same tasks, but it will only exist on a machine with the full IDE installed. I'm assuming in these instructions that you have the standalone engine installed on your box.
 
In the past, I would have written a quick VB script to perform this task. But it's a new modern world Sarcastic and I'm trying to mend my ways so I took off down the path of trying to write a C# task to do this, then I'm going to call this task from my nAnt build script.
 
Here's what I learned:
 
I started by using the Visual Studio IDE to create a C# console application. I wrote the code in there and got it to where it compiled and ran. Then I moved on to porting the code over into nAnt. You should be able to make a simple C# app like this in about 15 minutes.
 

1. Start the project. Open the IDE (I'm using Visual Studio 2005 Professional Team Edition). File > New > Project. Find Visual C# in the list. Choose Windows > Console Application. Fill in the desired project name and OK.

2. Make the project aware of the Standalone Automation Interface. Project > Add Reference.... Select the COM tab. Scroll down to InstallShield 12.0 Standalone Automation Interface. Click OK. Notice that in the Solution Explorer you now have about 10 different references listed. (This list will be important later when we need to repro these references in the nAnt script.)

3. Import the namespace reference for ease of use. At the top of the code file (program.cs) add a new using statement. This statement makes the project aware of the namespace so that as we use objects within the SAAuto12 namespace we don't have to keep prefixing them with "SAAuto12." every time. It's just a convenience thing.

using SAAuto12;

4. Open the ISM project. Declare a variable to hold the project object, then open it. Provide the full path to the ISM file you want to test this on.

ISWiProject project = new ISWiProject();
project.OpenProject(@"C:\projects\myISM.ism",false);

Note: Because my path string has slashes in it I preceded the string with the @ symbol so that the compiler won't think they are escape symbols. Also, there is no error handling in this yet, so if it can't open the file or if it opens it but it is not writable, too bad. Tongue out

5. For now, save and close project. Just to get something working quickly...

project.SaveProject();
project.CloseProject();
 

6. Build the solution and test it. Build the solution and make sure this compiles. Go out to the command prompt and run the executable. It should run without error.
 
WHEE!! We now have a simple C# app that is capable of using the InstallShield automation interface to modify an ISM. Now all you have to do is add some error handling and put the code to actually make the changes between OpenProject and CloseProject. I'll blog on an example of this later.
 
Next blog I'll cover how to take this basic code and put it into an nAnt script.

RunOnce Key needed for limited user

Most installations should be per-machine installations at this point. So with a per-machine installation say that you have a need to run something on the next reboot. Typically you'd put it in the HKLM RunOnce key. On Windows XP and Vista, limited or standard users don't have rights to delete from the HKLM RunOnce key.
Therefore, Windows won't even try to run whatever is in the HKLM RunOnce key.
 
So how do you effect a RunOnce in that situation??
 
Well, you could put the key into the HKCU, but this has problems, too.
1. Especially with the advent of Vista, it is HIGHLY recommended that you do not put user-specific stuff into your installer, for several reasons. Robert Flaming (former PM for the Windows Installer group at Microsoft has some valuable discussions about UAC and Vista and per-user problems on his blog.)
2. If you use the Registry table to create the HKCU key, beware! Keep in mind who the 'current user' is going to be at the time when the registry keys get added. If you are on Vista, and the user had to provide an admin's login in order to run the installer, then the 'current user' will be the admin, NOT the real user.
3. This process falls down if it so happens that the person who installs is not the next person who logs on.
 
Here are some options to try for a solution to this problem:
1. Don't do it. Find some way to refactor the installation so that the thing you are trying to put into RunOnce can be initiated by your product when it is first launched. It is really best practice to put this type of configuration into the execution of the product rather than in the installer.
2. Put the item in the HKLM Run key, but then find some way to either delete it from the Run key after it runs, or modify the item you are running so that once it's been run it just immediately exits if it gets run again.
3. Create a custom action that adds an HKCU RunOnce reg key but author the action so that it runs Deferred WITH the Impersonate bit set. This should cause it to treat the Current User as the *real* user and not the admin. Note, however, that this option has drawbacks! For one thing, it would be hard to clean this up on uninstall because you can never be sure who 'current user' will be on uninstall. In most cases the key should be gone by the time uninstallation happens, but... For another thing, this just illustrates why it is best practice to keep all your installation actions and data at the per-machine level.
 
 
Posted by SusanGorman with 1 comment(s)
Filed under: , , ,

Build and test a simple DLL

Continuing with the walkthrough of making a DLL from my previous blog...  
As a reminder, I'm following along with Chapter 19 (Dynamic Link Libraries) from Programming Windows 95 by Charles Petzold. 
 
I have a header file with a macro defined for exporting the function. Now I need to declare my function. The DLL from this book isn’t something I’ll use with installers, but it’s pretty simple and lets me get my feet wet. 
 
EXPORT BOOL CALLBACK EdrCenterText (HDC, PRECT, PSTR);
 
I typed this into my header file. Then took a step back to identify what these pieces parts all mean.
  • First we have the EXPORT macro that was defined earlier.
  • Next is the type of value that the function will return (BOOL).
  • The CALLBACK constant is something that I had trouble with. I did several searches in MSDN help and wasn’t finding any reference to an all caps CALLBACK. I finally got lucky and happened to have my mouse hovered over the word in the VS window. A tool tip popped up that said:

#define CALLBACK __stdcall.

Ooo! So this told me that CALLBACK is actually just a macro for the __stdcall calling convention. This convention has to do with how the function reads parameters off the stack and how it will clean up after itself. The __stdcall calling convention is… well… standard and in fact the Windows API uses it. In addition, I happen to know that we will want to use this convention when creating DLLs to be read by an installer. So.. CALLBACK is fine. 

And I have an important tip for myself -- go ahead and type the code in and use tool tips as an additional 'help' feature rather than trying to figure the code out before I type it in.  

That’s it for the header file. Moving on to the source code.  In Visual Studio I go to Source Files, Add, New Item. Then select the .cpp template and name my new file. We start off by including a few standard files, followed by our own header file.
#include <windows.h>
#include <string.h>
#include "edrlib.h" 

I’ve learned before that the <> symbols tell it to look for the header in a list of standard places, where as surrounding the filename in quotes tells it to look for it in the same directory as the .cpp file. 

Now I’ll type in the definition for the DllMain function. We don’t have any special actions needed to initialize this DLL so DllMain doesn’t do anything except just return TRUE. If I understand correctly, your DLL 'must' have a DllMain function so this should be pretty standard.

BOOL APIENTRY DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
 return TRUE; 
}

Now I’ll enter in the definition for the actual function in this DLL. The function takes some text in a window and centers it. Again, nothying I'd use in an installer so the details of this function aren't important. I'm just copying the code out of the book so I  won't repeat it all here.

After entering in the code I go wild and try to build it. (Silly me!)  I get an error (fatal error LNK1561: entry point must be defined).

It took me a bit of research to figure out what was needed here. Keep in mind that the book I'm using is 'old school' and assumes you are using a command line compiler and editing your code in notepad. So I'm having to figure out how to use Visual Studio to achieve the same result. Eventually I ended up making a project for a non-MFC DLL and comparing the resulting project properties between it and my test project. I realize that my only problem is that my project is defined as an EXE instead of a DLL (due to the fact that I started with an Empty generic project.) So to fix the build problem I go to the project properties, General tab and set the Configuration Type to .dll.

Sweet! Now it builds without error. In the output directory I see my DLL, a .exp and a .lib. Let's just take a moment to celebrate this minimal amount of success, considering that I'm *not* a C++ programmer. Smile

Now the test to find out if the function can actually be called from an external program. Still within my original DLLtest solution, I choose to create a New Project, Other Languges, Visual C++, General, Empty Project. At the bottom of that dialog is a Solution combo box. I select Add to Solution and OK. Now I have a single solution with a project for the DLL and a project for the EXE to test it.  Skimming over the next few steps - I just copy the code from the book to create a .cpp file for an EXE that opens a window and calls the function from the DLL to show text centered in the window. Now what my book tells me to do is:

  1. Include a reference to the header file from the DLL
  2. link the OBJ file from the DLL to the EXE (using a .MAK file) 
Since I'm using Visual Studio to do this, I did a little searching around and used some knowledge that I already happened to have and figured out that I needed to do these steps in my EXE project:
  • Add the directory that contains the DLL’s .h file to the Additional Include Directories on the C/C++ General property page.
  • Add the directory that contains the DLL’s .lib file to the Additional Library Directories on the Linker General property page.
  • Add the DLL’s lib file name to the Additional Dependencies on the Linker Input property page.

Once that's done I can build my DLL, then build my EXE. Then I run the EXE and voila! I get a window with the text centered in it. 

First major accomplishment is achieved!

Posted by SusanGorman with no comments
Filed under: , ,

What the heck is a __declspec?

(Note: I’m using InstallShield 12 for my install development and Visual Studio 2005 for DLL development.)

The goal is to learn to make a simple DLL that I can use in an MSI custom action. Today I started with the basics using a classic book that still has relevance today: Programming Windows 95 by Charles Petzold.

Chapter 19, Dynamic Link Libraries.  I got some basic concepts about dynamic linking vs. static linking, what a DLL is, then read though the source code for a simple DLL with a single function.

I open up Visual Studio, start a New Project. The Project Type is Other Languages , Visual C++, Empty Project. Under Solution Explorer, Header Files, I add a new (Code) Header File (.h).

I read the first line out of the book that it wants me to write into a .h file and already I’m lost. Too many keywords that I don’t understand the meaning or use of. The first line is:

#define EXPORT extern “C” __declspec (dllexport)

Huh? So I set about doing some research.  The #define statement I can understand.  But what about EXPORT? Is that a predefined keyword or what?  For some reason I picked the “extern” as the first thing to highlight and then hit F1 in Visual Studio. Eventually I made my way to this section of the MSDN help:

In the MSDN Library, Development Tools and Languages, Visual Studio 2005, Visual Studio, Visual C++, Programming Guide, General Concepts, DLLs, Importing and Exporting, Exporting from a DLL. *whew!*

This whole section was good and explained fairly simply the basic concepts about exporting and what our keywords in this statement were really doing. After looking through this section and also looking through a few sections about using “Windows Installer DLLs” and “standard DLLs” in the InstallShield 12 User Guide (downloadable from the Documentation section off their website), I feel that I have a good basic understanding of what we are trying to do with a DLL. So here’s what I came up with…

In order to setup the DLL so that the functions are exported and made available to external applications, we need to use certain directives that tell the compiler to export them. There is 2 ways to do this – one, use a .def file, two, define export directives and use them in the function declarations. Either method is fine. In C++ programming you are more likely to see the second method, so that’s the method I’m going to use.

So we are going to define some export directives. We *could* put these directives right in the declaration for each function, but to make it simpler, we’ll setup a macro with the directives we want, then just use that (much shorter) macro in front of each function.

So in the header file start off by defining the macro with this code:

#define EXPORT extern “C” __declspec (dllexport)

This defines a macro so that where the term “EXPORT” is used in your function declarations, it will be (essentially) replaced with the whole string that follows it in the above line. So what is all the rest of the string for?

  • “extern” tells it to make the function available to external programs
  • “C” tells it that we have a C++ function that we want to be accessible to C language modules. This avoids function ‘name decoration’ that would otherwise be used when compiling for C++ language modules.
  • “__declspec (dllexport)” allows us to skip having to create and use a .def file for exporting.

By the way I've used "EXPORT" but you could use any term that's valid as a macro name. 

Ok, so far so good. I feel I’ve made some progress at learning the fundamental concepts here, so it seems like a good spot to stop.  Next time I’ll tackle actually declaring a function!

Posted by SusanGorman with 1 comment(s)
Filed under: , ,

About me... the long version

Let me introduce myself. I'm Susan Gorman. I'm a professional install developer. I've been a full-time install developer for about 10 years now. It all started innocently enough...

Back in 1990 I lived in Kirkland, Washington about a stone's throw from this little startup company called Microsoft. I caught the fever and decided to buy my first computer, from Cosco, no less. It came with this cool little book called "MS-DOS" and Windows 3.1 (if I recall correctly). I'm one of those weird people who actually *reads* the documentation -- so... I started reading the DOS manual. When I finished (and yes, I did read it cover to cover!), I was all set to use basic DOS commands. Whee!!!

Fast forward a couple years. I'd been working in the mortgage and banking industry for a bit and I heard about a company that was developing software that would automatically fill out all those long and tedious mortgage application and processing forms for you. No more typewriters? No more carbonized forms? This I had to see! Other than using my own personal computer at home I had no formal computer-related education, but I was detail-oriented, well-spoken, literate, and a logical thinker. They thought I'd make a great support desk person. And off I went into the world of software development! I slid happily from support desk to trainer to documentation writer.

In 1993, I moved to Tallahassee, Florida and landed a job in QA. I didn't know anything about SQL or networks or even testing, per se, so the job interview wasn't looking too good, until I discovered that my prospective manager and I both loved The Princess Bride and Monty Python. I think it was "Ah, but I know something you don't know! I'm not left-handed!" that got me the job. I continued gaining on-the-job experience in QA. Touched a bit on Unix systems and some SQL. Even a little automated testing scripts. Then somebody got the bright idea of having me test the translated versions of the product. It's amazing how easily you can actually work on German, French or Italian Windows even when you don't speak the language.

At one point, I became the defacto Configuration Management department (after I looked up what Configuration Management was) and started managing the production of builds and master media. Then somebody tasked me with the the job of creating a kit from which third-party vendors could localize and translate our product. I got it all put together except for one last bit -- the install program. No one knew how to localize or translate it. The one developer who had *reluctantly* written the install program using InstallShield 2 or 3? refused to even touch the thing again. So.. I dove in. I figured it out. I became the new "install developer" because no else was willing to touch the thing.

After a year or so I finally convinced management that I ought to get some formal training on this InstallShield thing. I attended IS 5 basic and advanced training sessions. That helped a lot. I discovered the InstallSite web site and learned that I wasn't alone! Other people were slogging along writing installers on limited resources. Over time I gained experience and 'leveled-up' through InstallShield 3, 5, 5.5, 7, 8. The installation department at my company grew over 10 years to a staff of 5 full-time install developers. We were legion! And then came the revolution - Windows Installer. Egad! We had no idea what this thing was. We delayed switching from InstallScript to Windows Installer as long as possible. But finally I had no choice.

In 2005 I finally left the company I'd been at for 12 years and took on the challenge of being the sole Install Developer and Configuration Manager at a way-cool company working in the anti-malware industry, Sunbelt Software. I now had to figure out what MSI was and what to do with it. Believe it or not, there are very few books that actually focus on Windows Installer itself. Thank you, Bob Baker.

 
So now I've had a few years of MSI development under my belt. With the advent of Vista, it's become apparent that "this ain't your daddy's deployment process" anymore. The little tricks and comfortable techniques that I'd developed over the years that worked, but weren't really 'best practice' -- they won't fly anymore. I'm not a C++ developer (or any kind of Visual Studio developer). SO when I need custom action functionality, it's always been VBScript for me, baby. And the vast majority of the time, it works. It seems, however, that the time has come for me to make the leap. It's time to to take my install development work to the next level, to learn to write custom actions in native code (C++), to truly rise up and become one of the "MSIdle".
 
As I make this journey, I'll document my struggles, findings, a-ha's!, and other whatnots. I hope this blog becomes a source of information and community for us.
 
Welcome, and thanks for joining me!
Posted by SusanGorman with no comments
Filed under: