Visual C++ Tutorial


The code in this example builds a command line application called AddUser that can add a user to a PI3 system. It takes command line arguments specifying the server, the new user and the name of a group the user is added to. The code makes use of the built-in COM support in Visual C++ that builds smart pointer classes that wrap the PI-SDK interfaces. The example was built with Visual C++ Version 6.0.

Step 1:

Open Visual C++ and select New from the File menu. Select the Project Tab and choose Console Application. Give it the name AddUser. Make sure the file location shown is the desired location for your source files and click OK. Choose empty project from the next dialog and allow the wizard to finish.

Step 2:

From the File menu choose New again, this time selecting the Files tab. Choose C++ Source File and give it the name AddUser.cpp. The "add to project" check box should remain checked.

In the new file, AddUser.cpp, insert the code shown below and save the file. After the settings are modified and the project is built in the steps below, this code is explained.

#include <windows.h>
#include <atlbase.h>
#import "pisdkcommon.dll" no_namespace
#import "piTimeServer.dll" no_namespace
#import "pisdk.dll" no_namespace 

// prototypes
bool ParseCommandLine(LPTSTR szCmdLine,_bstr_t & bServerName,
_bstr_t & bUserName, _bstr_t &bGroupName);

main()
{
   LPTSTR szCmd = GetCommandLine();
   _bstr_t bServerName,bUserName,bGroupName;
   if(!ParseCommandLine(szCmd,bServerName,bUserName,bGroupName))
   {
      _tprintf(_T("Usage: AddUser Server User Group\n"));
      return 0;
   }
   CoInitialize(NULL);
   try
   {
      IPISDKPtr pPISDK(__uuidof(PISDK));
      ServersPtr pServers;
      ServerPtr pServer;
      PIUsersPtr pUsers;
      PIUserPtr pUser;
      PIGroupsPtr pGroups;
      PIGroupPtr pGroup;

      pServers = pPISDK->Servers;
      pServer = pServers->Item[bServerName];
      // verify the group exists
      pGroups = pServer->PIGroups;
      pGroup = pGroups->Item[bGroupName];
      // Build a NamedValues collection with the group
      _NamedValuesPtr nvGroups(__uuidof(NamedValues));
      _variant_t vtTemp;
      vtTemp = (long)1; // not used
      nvGroups->Add(bGroupName,&vtTemp);
      // Get the Server's PIUser collection to add the user
      pUsers = pServer->PIUsers;
      _bstr_t bPassword = L"";
      _bstr_t bContext = L"default";
      pUsers->Add(bUserName,(_bstr_t)L"Newly Added User",
      bPassword,nvGroups,bContext);
   }
   catch(_com_error Err)
   {
      _tprintf(_T("Error:%s : 0x%x \n"),(TCHAR *)Err.Description(),Err.Error());
   } 
   CoUninitialize();
   return 0;
}


bool ParseCommandLine(LPTSTR szCmdLine,_bstr_t & bServerName,
                                _bstr_t & bUserName, _bstr_t &bGroupName)
{
   // skip over program directory
   LPTSTR szPtr = _tcstok(szCmdLine,_T(" \n\0")); 
   if(!szPtr)
      return false;
   szPtr = _tcstok(NULL,_T(" \n\0")); 
   if(!szPtr)
      return false;
   bServerName = szPtr;
   szPtr = _tcstok(NULL,_T(" \n\0")); 
   if(!szPtr)
         return false;
   bUserName = szPtr; 
   szPtr = _tcstok(NULL,_T("\n\0")); 
   if(!szPtr)
      return false;
   bGroupName = szPtr; 
   return true;
}

Step 3:

In the Project window, select the FileView tab and select AddUser files, which should be bold, as it is the current project. From the Project menu, select Settings. In the combo box at the top left labeled "Settings For:" select "All Configurations." In the tree view underneath the combo box select the project "AddUser." From the tabs in the right pane select C/C++. In the combo box in that pane labeled "Category:" select "Preprocessor." In the third text field labeled "Additional include directories" enter the directory or directories where the files pisdk.dll, pisdkcommon.dll, and piTimeServer.dll are found. Typically this will be something like c:\program files\pipc\pisdk and c:\program files\pipc\library. You can add multiple directories separated by commas. Click the OK button.

Step 4:

From the Build menu select "Build AddUser.exe", or typically you can press F7 to perform the same function. The project should build without errors. If you encounter errors you can focus on each one by pressing F4. This will display the error in the status bar at the bottom of the application and bring up the source line that caused the error.

To run the code from the command line, bring up a command window and change directory to the location of the executable. If you have built in Debug, which is the default, the executable will be in the Debug subdirectory of the location you entered for the files in the project wizard. Enter the program name followed by the arguments separated by spaces. The arguments are; a server name from your Known Servers Table, a new user name to add to the server, and a group name for the new user:

AddUser myserver newuser piuser

To step through the code in the debugger you need to define the command line arguments in the project settings. Bring up the Settings dialog from the Project menu and again select the project in the left pane and choose the Debug tab in the right pane. In the combo box at the top select General. In the third text entry field from the top, labeled "Program arguments:" enter the three arguments for this program separated by spaces. Click OK to commit the settings.

Set a breakpoint in the code at the first executable line (the one calling GetCommandLine) by putting the cursor in the line, clicking the right mouse button and selecting "Insert/Remove Breakpoint." Start the program by selecting "Start Debug" from the Build menu and the item "Go" from that submenu. The program should begin executing and stop at the first line of code where the breakpoint was set. You can then step line by line through the program using the "Step Over" command from the Debug menu.

Code Discussion:

The code starts with #include statements:

#include <windows.h>

#include <atlbase.h>

#import "pisdkcommon.dll" no_namespace

#import "piTimeServer.dll" no_namespace

#import "pisdk.dll" no_namespace

The first two files provide the Win32 and ATL definitions necessary to support WIN32 programming on the O/S using ATL helper classes.

The next three lines:

#import "pisdk.dll" no_namespace

#import "piTimeServer.dll" no_namespace

#import "pisdk.dll" no_namespace

bring in the PI-SDK functionality by constructing wrapper classes for the interfaces. This will generate two files for each import (for example for pisdk.dll the files pisdk.tli and pisdk.tlh) in the Debug or Release subdirectory of your project, depending on the build type. The code below will use these wrapper classes to access the PI-SDK functionality. Complete details of the wrapper classes are beyond the scope of this document but can be found in Microsoft documentation and by reviewing the generated files. The no_namespace qualifier on the end of the import statement lets us use the wrapper classes without prefixing the names with the library identifier (for example PISDK::). This is convenient and works as long as there are no collisions with other names used by the application.

Next the code defines a prototype for a command line parsing function, followed by the main function. The program does not use argc and argv for command line arguments, as they are not Unicode compliant. Instead, the code calls the Win32 GetCommandLine function to retrieve the program arguments. This is followed by a call to a parsing function that returns true if successful. If command line parsing is unsuccessful, the program prints a Usage statement and exits. The parsing function is found at the bottom of the code and simply uses the strtok function to walk the provided command line, storing the tokens into passed _bstr_t references.

There are two interesting items in the parsing function, ParseCommandLine. First the actual parsing call is to _tcstok that will switch between the Single Byte, multi-byte, and Unicode implementations based on the compiler predefined constants (none for Single byte, _MBCS for multi-byte, _UNICODE for Unicode) resulting in calls to strtok, _mbstok, or wcstok respectively. This coding style has been used throughout when dealing with strings as it creates code that can be compiled for different character representations. The Wizard project implementation defined _MBCS by default. The other point of interest in the function is the use of _bstr_t. This is a helper class from Microsoft defined in the file comutil.h. It wraps the BSTR data type and handles calling SysAllocString during construction and SysFreeString on destruction for BSTR's as necessary and converts between Unicode and MBCS as needed using cast operators. This saves a lot of coding and potential leaks by simply allowing the rules of variable scope to control the allocation and release of the BSTR's. It also handles character type conversions transparently. Another class _variant_t defined in the same header file is used to provide similar types of support for the VARIANT data type. Both are used in this code sample.

Having parsed the command line successfully, the code moves on to call CoInitialize(). All COM programs must call CoInitialize, CoInitializeEx, or OLEInitialize before making COM calls, and should call CoUninitialize before exiting when no more COM calls will be made. This call establishes the "apartment" in which the COM objects will be instantiated. By calling CoInitialize you are implicitly directing COM to create a single threaded apartment (STA) for this thread where all the COM objects created by this thread will reside. Other threads that wish to communicate with objects in this STA will have their calls marshaled into this apartment and execute on this thread. This marshalling and the implementation of the STA have the effect of serializing all calls to the objects in the apartment. To associate a different apartment model with this thread the CoInitializeEx function is used. The PI-SDK objects are marked as supporting the single threaded apartment model so calling CoInitializeEx with a different setting, (e.g. multi threaded apartment MTA), will force COM to create a separate single threaded apartment and marshal your object calls from your thread to this COM created apartment where the objects live. Note Visual Basic programs will automatically use an STA. Detailed discussion of apartment threading models, instantiation and marshaling is beyond the scope of this document.

Following the call to CoInitialize we enter a try/catch block. The underlying COM calls each return an HRESULT that indicates the error status. The Visual C++ generated wrapper classes handle these errors by throwing exceptions of type _com_error. You can see at the end of the try block a catch statement for handling these errors. The code in the catch block prints out details of the error by simply accessing member functions of the thrown data member. Behind the scenes, the wrapper code is receiving the HRESULT, determining it is a failure and then querying the object to see if it supports extended error information. If the object contains this support (PI-SDK objects do), then the underlying code obtains the latest IErrorInfo interface from the system and builds a _com_error object that contains a pointer to the interface. The _com_error object methods provide the interface to the extended error information by calling the proper IErrorInfo methods. The _com_error object destructor takes care of releasing the IErrorInfo interface.

All this underlying code greatly simplifies error handling and reporting. In the code following the try statement, as we make COM calls using the wrapper classes, any errors will send us to the catch block and we can process the error conveniently there or even re-throw the _com_error to a higher level handler. The code in this sample relies on the PI-SDK generating useful error descriptions. Rather than testing the actual codes and displaying custom messages, it just displays the PI-SDK error description and exits. More complicated applications would expand on this error handling to take corrective action or provide the user with options.

Following the try statement we encounter the first PI-SDK code. The line,

IPISDKPtr pPISDK(__uuidof(PISDK)); 

creates the main PISDK "app" object and returns a pointer to the PISDK interface. This object is created automatically in Visual Basic applications and never needs to be created or referenced directly. The members of this object appear as globals in Visual Basic. The IPISDKPtr data type is a "smart pointer" created by the wrapper classes based on _com_ptr_t. The wrapper classes create these smart pointer classes for each interface and name them with the interface name with a Ptr suffix.

The code here calls the IPISDKPtr constructor passing the CLSID of the object (__uuidof(PISDK)). This allows the underlying code to create the object based on the CLSID and obtain a pointer to the interface IPISDK. The interface type is known because the wrapper class IPISDKPtr is built specifically for this interface.  The __uuidof method is a Microsoft specific extension that provides the GUID of the passed class. This avoids having to include GUID definitions such as those provided in pisdk_i.c.   When the smart pointer (pPISDK) goes out of scope, the interface is released automatically. True to COM, when the last reference is gone the object is destructed. Using smart pointers avoids calls to CoCreateInstance, QueryInterface, AddRef, and Release. For details on the various constructors and methods of the smart pointer classes see the Microsoft documentation for _com_ptr_t.

After obtaining the pointer to the main PISDK object, we declare smart pointers for the other interfaces we will be using. The order of the declarations is unimportant and could have been done before we obtained the IPISDK pointer. Note how the names of the data types correspond to the default interfaces for the PI-SDK objects with a Ptr suffix.

Next we obtain the Servers interface from the main PISDK object with the line

pServers = pPISDK->Servers; 

The smart pointer classes and the wrappers work together and replace the need for a few lines of code calling QueryInterface and doing error checking, with a single line that looks like a member call that returns an interface. This is much more readable, contains less code and so is likely to contain fewer bugs. When the interface goes out of scope it is automatically released, avoiding a common source of leaks in COM code.

We next want to obtain the server whose name was passed on the command line from the Servers collection. We need to use the Item method to obtain the collection member and the wrapper classes have provided bracket operators to make the Item method look like an array access

pServer = pServers->Item[bServerName]; 

If the passed server name was not found, an error would be generated and an exception thrown. If we did find the named server, the pServer pointer now contains the Server interface. We obtain the PIGroups member of the interface much as we obtained the Servers collection from the PISDK interface and then we call the PIGroups.Item method passing the named group.

pGroups = pServer->PIGroups; 

pGroup = pGroups->Item[bGroupName]; 

We do this to ensure that a group of the passed name actually exists on this server. If it doesn't the underlying classes will again generate an exception.

We know we want to eventually call the PIUsers.Add method to add the new user to the server. This method takes as an argument a NamedValues collection containing group names for the new user. We next build this collection. In our case we have only allowed for one group in our command line input but we still build a collection with one member. The NamedValues object is a "creatable" object. This means we can create one, independent of other PI-SDK objects. The main PISDK object we constructed as our first PI-SDK call is also a creatable object. The other object references (interface pointers) were obtained by calling methods of other PI-SDK objects. These objects (Servers, Server, Groups, Group) are not creatable independently. They can only be obtained by asking for them from other objects as we have done. This is typical of an object hierarchy. The NamedValues object is a helper object and is independent of the hierarchy. In fact in PISDK1.1 this object is in a separate COM Server, pisdkcommon.dll. We obtain a new NamedValues object to build our collection of group names using a smart pointer constructor, much like the creation of the main PISDK object.

_NamedValuesPtr nvGroups(__uuidof(NamedValues)); 

Note the NamedValues interface actually has a preceding underscore. This differentiates it from the creatable object. In Visual Basic the underscore is hidden.

To add values to our NamedValues collection we use its Add method, which takes a BSTR and a pointer to a VARIANT for the name and the value respectively. We already have the group name from the command line as a _bstr_t that has a cast operator to return a BSTR. For the VARIANT we use the wrapper class _variant_t that takes care of initializing and destroying a wrapped VARIANT much like the _bstr_t. The PIUsers.Add method only uses the name portion of each NamedValue, so we need not store a value in the _variant_t (it is initialized as an empty VARIANT.). In the code here we have arbitrarily used a value of 1 as a long. Then we call NamedValues.Add, which builds a new NamedValue object from the passed arguments and adds it to the NamedValues collection.

_variant_t vtTemp;

vtTemp = (long)1; // not used

nvGroups->Add(bGroupName,&vtTemp); 

Next we retrieve the PIUsers collection from the server in much the same way we retrieved the PIGroups collection.

pUsers = pServer->PIUsers; 

We construct strings as _bstr_t objects for the password and context string to pass to the PIUsers.Add command

_bstr_t bPassword = L""; 

_bstr_t bContext = L"default"; 

We use the L prefix here to generate a wide character (Unicode) string. The _bstr_t class can be constructed with multi byte characters also but all COM calls use Unicode and the Add call takes BSTR's so when we pass the _bstr_t as an argument it would call its BSTR cast and have to translate to Unicode if we didn't start with the Unicode string.

At last we make the call that performs the program's intended function:

pUsers->Add(bUserName,(_bstr_t)L"Newly Added User",bPassword,nvGroups,bContext); 

If no exception is thrown (we don't enter the catch code) then we have successfully added the new user.

Finally we call CoUninitialize to close down our apartment and clean up resources and exit the program.

While the discussion of the sample code may be lengthy, using the built-in COM support makes the actual code concise, intuitive, and readable. Of course you can program the PI-SDK in C and C++ by making explicit CoCreateInstance, QueryInterface, AddRef, Release, etc. calls as well. This may be necessary if you don't have access to Microsoft compilers.

Enabling Operational Intelligence