2.3. Interface Between Visual Basic and Visual C++

2.3.1. Introduction

In the last section, we discussed the advantages of calling a DLL from within Visual Basic. This calling style is especially useful in real-time control and measurement applications, such as automated test equipment, automobile manufacturing, real-time control and process, as well as semiconductor inspection and process. In those applications, time and speed are critical factors to successful implementations. Visual Basic provides good environments (Graphic User Interfaces) between the human and machine, and it integrates separate tasks to an Object-Oriented-Programming environment and connects them with a sequence of events. Visual Basic is a machine-independent language, and it can be used for any operating system as long as a Visual Basic run-time engine is provided. But Visual Basic does not have a powerful ability to access low-level components, such as device drivers, I/O manager, Hardware Abstraction Layer (HAL), and even the hardware. So a medium or an interface is needed to connect between Visual Basic and low-level components. C/C++ is a good candidate for this interface.

As we know, C/C++, especially Visual C++, is a very good language applied in low-level applications. Most device drivers are developed in C. Even Windows API libraries that we employed in the previous section are developed in C. Visual Basic can access a C/C++ program by calling either static or Dynamic Link Libraries.

Dynamic Link Libraries can be loaded during the program running stage, not the program compiling stage. This provides more flexibility for the applications and saves memory space. Also, the DLL can be shared by multiple tasks, which is a popular technology today.

By combining Visual Basic and DLL developed in C/C++, we can conveniently access low-level components and still keep the advantage of using the good GUI provided by Visual Basic. Both user-friendly interface and real-time controllability can be obtained at the same time.

Generally, the DLL can be divided into two categories: Win32 general-purpose DLL and MFC-based DLL. The former can be developed in either the C environment or the Visual C++ environment, but the latter can only be developed in the Visual C++ environment. The Win32 general-purpose DLL has a relatively wider range of implementations, and it can be called by most third-party programs, such as Visual Basic, Java, Perl, and SmallTalk. The MFC-based DLL can only be accessed by a limited number of languages, such as Visual C++ and Visual Basic. But the MFC-based DLL has an advantage compared with the Win32 general-purpose DLL: The former can access objects and classes defined in the MFC library.

In order to meet the needs of general-purpose applications, we will concentrate on the development of the Win32 general-purpose DLL. To implement a Win32 general-purpose DLL, which is similar to calling a Window API DLL as we discussed in the previous section, you need to declare the DLL function prototype. This declaration is similar to earlier declarations, but it is not exactly the same. Besides the declaration, you also need to first develop a user-defined DLL and make a connection between Visual Basic and Visual C++. We will discuss this issue in detail next.

2.3.2. General Requirement for Calling a User-Defined DLL

To successfully call a user-defined DLL from within Visual Basic, you need to handle two kinds of jobs: develop a user-defined DLL in the Visual C++ domain, and declare and map the DLL functions in the Visual Basic domain.

Let's take a look at these two issues separately. First, in order to develop a Win32 general-purpose DLL, you need the following files.

SOURCE FILES

  • Source files define and develop the source code for the DLL. This source code contains all DLL functions and local functions, as well as all functionalities of the DLL.

HEADER FILES

  • Header files declare the prototypes for all DLL functions, local functions, and other components, such as structures and structure pointers. These header files are necessary if the DLL functions are to be accessed by some programs developed in C/C++. You have to place these header files into the calling programs to provide prototypes for all DLL functions.

DEF FILE

  • The DEF file, or definition file, exports all DLL functions developed in the source files to the outside world. Third-party programs cannot access any DLL function without this definition file. Based on this DEF file, third-party programs can easily identify any DLL function developed in either C or Visual C++. Some calling programs developed in C/C++ don't even need this DEF file; they can still find the DLL functions they need.

LIB FILE

  • The LIB file, or library file, provides a path for the calling program to locate the DLL file when the calling program is compiled. Without this library file, the compiler cannot recognize any DLL function as it compiles the calling program. This library file must be inserted into the calling program and linked with the calling program to obtain the target file if the calling program is developed in C/C++.

DLL FILE

  • This is our final product for the DLL project. Basically, a DLL file is similar to an executable file (.exe), but it cannot run by itself. You need a calling program to call that DLL file to access and run DLL functions that are located in that DLL file. This DLL file should be placed in a searchable path in your machine, such as in C:WINDOWSSYSTEM. In this way, the computer can automatically find that DLL file and dynamically connect it with your calling program. You can define a user-defined DLL directory and save the DLL file under that directory, but you have to make that user-defined DLL directory a searchable directory on your system. Otherwise, your computer will not know where to find that DLL file, and a Cannot find the DLL error would be displayed on screen.

In order to declare and map DLL functions in the Visual Basic domain, you need to declare the prototype of DLL functions. This declaration is similar to earlier declarations when calling a Windows API DLL function. One difference is the library's name clause. You should place the full name of the library, which includes the complete path and library name, in this clause. For example, say you developed a DLL file named SimpleDLL.dll and saved this file in the directory C:stdll. You would need to place the full name of that library, C:stdllSimpleDLL.dll, in the library's name clause.

Let's start with a simple example project. In Chapter 4, we will develop a DLL file in the Visual C++ domain named STRandomDLL. The purpose of that DLL file is to interface to Smalltalk and compute the maximum, minimum, and mean values for a random sequence. Here, we want to use that DLL file, with some modifications, in our Visual Basic project.

The purpose of this simple example is to create a Visual Basic project to accept the user's inputs, for the amplitude and number for a random sequence. Then, we create the random sequence in the Visual Basic domain. Visual Basic will call the DLL developed in Visual C++ to perform the calculations for the maximum, minimum, and mean values for that random sequence. The calculated results will be returned to the Visual Basic domain by passing a structure. Finally, the calculated results will be displayed in three textboxes in the Visual Basic domain.

2.3.3. A Simple DLL Example

First, let's develop a DLL project in the Visual C++ domain. Launch Visual C++ 6.0 and create a new project with type Win32 Dynamic-Link Library, and enter VBSimple into the Project name: input field. Make sure that the content of the Location: field is our user-defined project folder, C:Vbdll. Click OK to open this new project.

In the newly opened project, click File|New to create a new C/C++ Header File, enter VBSimple in the File name: field, then click OK to open this header file. Enter the code shown in Figure 2-31 into this file.

  1. First, we define a maximum number of elements stored in the random data array. This number is dependent upon your application, and can be larger or smaller than the number in our example.

  2. Some global variables that will be used in this DLL project are declared in this header file.

  3. A data structure Result is also declared here. The purpose of this structure is to return the calculated results of the random sequence to the Visual Basic program. The structure itself can be considered as a user-defined data type, as we discussed in the previous section. Three double variables are defined in this structure, max, min, and mean, representing the maximum, minimum, and mean values of the random sequence, respectively.

  4. The convert() function is a local function used to convert double data to formatted data with 3 significant digits after the decimal point.

  5. We define a macro DllExport to represent the standard DLL export statement, _declspec( dllexport ). In this way, we can save space in the following declaration and definition for this standard DLL statement. You must use this macro or standard DLL statement to tell the compiler that you want to export the following functions as the DLL functions.

  6. Two DLL functions are declared in this project. The randCalc() is used to calculate the statistic result for the passed random sequence, and the randResult() is used to return the calculated results to the Visual Basic domain.


Figure 2-31.


Now, click the File|New menu item from Visual C++ 6.0 to create a new C++ Source File. Enter VBSimple in the File name: field, and click OK to open this new source file. Enter the code shown in Figure 2-32 into this new source file.

Figure 2-32.
						/******************************************************************************
						* NAME       : VBSimple.cpp
						* DESC.      : DLL file for calculation of random array result, called by Visual Basic.
						* DATE       : 6/30/2002
						* PGRMR.     : Y. Bai
						******************************************************************************/
						#include <windows.h>
						#include <cstdio>
						#include <cstdlib>
						#include <cstring>
						#include <cmath>
						#include <iostream>
						#include "VBSimple.h"
						using namespace std;
						Result cResult;
						DllExport int CALLBACK randCalc(int size, double rdataArray[])
						{
						int i;
						m_max = rdataArray[0];
						m_min = m_max;
						for (i = 0; i < size; i++)
						{
						if (m_max < rdataArray[i])
						m_max = rdataArray[i];
						if (m_min > rdataArray[i])
						m_min = rdataArray[i];
						m_mean = m_mean + rdataArray[i];
						}
						m_mean = m_mean/size;
						return 0;
						}
						DllExport Result CALLBACK randResult()
						{
						cResult.max = convert (m_max);
						cResult.min = convert (m_min);
						cResult.mean = convert (m_mean);
						return cResult;
						}
						double convert(double input)
						{
						char buf[128];
						sprintf(buf, "%.3f", input);
						return atof(buf);
						}
					

The code entered here is straightforward and doesn't need further explanation except for the CALLBACK keyword. Both DLL function declarations contain a CALLBACK keyword. When writing code in DLLs, it is important to remember that Stack Segment ! = Data Segment. This is an issue when an application function passes a pointer to one of its local variables to the DLL function. In Microsoft C++, all NEAR pointers are assumed to be relative to Data Segment. The way to avoid problems is to either


  • use a function prototype declaring the parameter to be a FAR pointer, or

  • explicitly cast the parameter to FAR.

DLL functions that will be called from applications should be declared with the FAR modifier because an intersegment jump is required from the application. Functions that are only called from within the DLL may be declared NEAR. The PASCAL or _cdecl calling conventions may be used as appropriate. Functions called from Windows must be declared using the PASCAL calling convention.

To summarize, a DLL function called by other applications should be cast by a keyword, FAR PASCAL. In Visual C++, the CALLBACK macro is equivalent to this FAR PASCAL cast. That is why you must place the macro CALLBACK just in front of the DLL function name.

We need to do one more thing before we can build our project—that is to create a DEF file, or definition file. Click the File|New menu item from Visual C++ 6.0, create a new Text file, enter VBSimple in the File name: field, and click OK to open this new text file.

We want to create a DEF file, not a text file. But in Visual C++ 6.0, you cannot find a file type of DEF, and the DEF file is similar to a text file. So we need to do the following conversion to translate that text file to a DEF file.

Click Project|Add To Project|Files. . . to open the Insert Files into Project dialog box. Select All Files (*.*) from the Files of type: list box and find the text file VBSimple.txt you just added. Right-click on this text file and select Rename from the pop-up menu. Replace the file extension .txt with .def and click OK. Click Yes in the message box to confirm that we want to rename this file. The text file VBSimple.txt has been renamed VBSimple.def, as shown in Figure 2-33.


Figure 2-33.


Now, click the converted VBSimple.def file to select it, and click OK to add this DEF file into our project. From the Visual C++ 6.0 workspace, you will find that this DEF file has been added into the project. Click on the VBSimple.txt file from the Visual C++ 6.0 workspace to select it, then click Edit|Delete to remove this text file because we no longer need it.

Open the converted DEF file and enter the code shown in Figure 2-34 into this file.

Figure 2-34.
						LIBRARY VBSimple.dll
						EXPORTS
						randCalc
						randResult
					

Click Build|Build VBSimple.dll to build our DLL file. You can change the Active Configuration from Debug to Release and build the terminal DLL file in there. If everything is correct, your terminal DLL file should be located in either the Debug or Release folder, depending on your Active Configuration selection. Open the Active Configuration folder and copy the destination DLL file, VBSimple.dll, to our user-defined DLL directory—in our case, C:stdll. This is a searchable directory on our computer system. You can copy this DLL file directly to the system DLL directory, such as C:WINDOWSSYSTEM, if you don't like to create a user-defined DLL directory. The computer will automatically find and locate this DLL file as you run your program.

Now let's develop a Visual Basic project that calls the DLL file we just created. Launch Visual Basic 6.0 and create a new Standard EXE project. Click OK to open this newly created project. Click File|Save Form1.frm As to save Form1 as VBSimple.frm, and click File|Save Project As to change the project's name to VBSimple.vbp. Click the Form VBSimple and add the following components to this form. Figure 2-35 shows the resulting screen.


Figure 2-35.


ComponentNameCaption
LabelLabel1Visual Basic For DLL Calling Test
Text boxtxtMax 
Text boxtxtMin 
Text boxtxtMean 
Command buttoncmdCalcCalculate
Command buttoncmdExitExit
Command buttoncmdCreateCreate
LabelLabel2Maximum
LabelLabel3Minimum
LabelLabel4Mean Value
FrameFrame1Calculated Results
FrameFrame2User Inputs
LabelLabel5Amplitude
LabelLabel6Number
Text boxtxtAmp 
Text boxtxtNum 

Click the View Code button to open the code window. Click General from the Object list to open the Form's General Declaration section, and enter the code from Figure 2-36 into this section. Please note that all variables and functions you declared here are the Form's level variables or the Form's level functions, which means that the scope of variables and functions defined in this section is the current Form. In other words, those variables and functions can be accessed by all event procedures defined in the current Form, VBSimple.frm. Any other event procedure defined in other Forms (even in the current project) cannot access those variables and functions.


Figure 2-36.


The purpose of the Option Explicit statement is to tell the Visual Basic interpreter that all variables and functions declared in this Form are unique, and any variable used in this Form must be first declared, otherwise, an error message will be displayed for any variable that has been used without being declared.

In Visual Basic 6.0, it is legal to use a variable in the program without declaring it prior to use. Of course, this is convenient to users, but this also brings up a problem. The program would be absolutely wrong or even crash your computer if the user, thinking that a variable is a valid predeclared variable, used that variable and either the variable's name was misspelled or it was out of scope.

In order to solve this problem, the Option Explicit statement is used here to make sure that all variables used in this Form are unique and have been declared before they are used.

  1. The first variable we declare for this project is the random data array, dArray(), with a data type of double. Because the size of this random array will be determined by input entered by the user later, we cannot provide a definite size for this array right now; therefore, we create a data array with an uncertain dimension here.

    The num is an integer in the C/C++ domain that represents the size of the random array and will be entered by the user later. Because of the difference between integers defined in the C/C++ and Visual Basic domains (4-byte in C/C++ and 2-byte in Visual Basic), a Long type (4-byte in VB) is defined for this variable to match an integer defined in the C/C++ domain.

  2. A user-defined data type, Result, is declared here, which is a structure and is used to store the returned calculated results for the random sequence from the DLL developed in Visual C++. Three double variables are defined in this structure, and each one represents a statistical result for the random array.

  3. Two DLL functions are declared here. The first one is randCalc(), which is defined in the DLL named VBSimple.dll. It is used to perform a calculation for the random sequence. This DLL function has two arguments, size, the dimension of the random array and passed by value, and dArray, an entire data array to be passed into the C/C++ domain, so a reference to the data array is passed (pass by reference). Because passing by reference is the default passing mode, you don't need to place a keyword before the variable dArray. This function needs to return an integer to indicate whether this function call is successful. Similarly to how the variable size was declared, a Long type is used for this function and is attached at the tail of this function.

  4. The second function to be declared here is randResult(), which is also defined in the DLL VBSimple.dll. The purpose of this function is to return the calculated result from Visual C++ to the Visual Basic domain. No argument is applied for this function, and the returned type for this function is a user-defined data type Result, which is attached at the tail of this function.

One point we need to emphasize is that the DLL file should be stored in a searchable path on your computer if you declare the DLL function in this way (only the DLL name, VBSimple.dll, is provided at the DLL name field). Otherwise, you should provide a full name for your DLL (including the path and DLL name). You can even delete the extension (.dll) in the DLL name and only provide the DLL name as "VBSimple" in the name field.


Another point is that you should declare all variables or user-defined data types that will be used by the DLL functions as arguments before you declare the DLL functions. Otherwise, the Visual Basic interpreter will give an error message.

Now, click cmdCreate from the Object list to open the Create command button's event procedure, and enter the code shown in Figure 2-37 into this procedure.

  1. Two local variables, n and amp, are declared as integers at the beginning of this procedure. The variable n is used as a loop variable to create a random sequence later, and amp is used to store the Amplitude of the random sequence and is entered by the user as the program runs.

  2. A system function Val is utilized to pick up and convert two user-input variables, Amplitude and the Number—which represent the magnitude and the amount of the elements of the random sequence to be created—from the text string entered by the user to the integer values, and assign the results to two variables, amp and num, respectively.

  3. After the Amplitude and Number are obtained, we need to check these two parameters to make sure that we have gotten valid parameters to continue to create a random array based on these two values. First, we check the Amplitude value. A normal value for this parameter should be a number greater than 0. Otherwise, a message box with an error message will be displayed to report this error, and the program will be terminated.

  4. Similar to step C, we also need to check the Number parameter to make sure that this parameter is in a valid range. In our case, we defined the valid range for this number as between 0 and 5000.

  5. Recall in step A in Figure 2-36 that when we declared the random data array, we didn't exactly know the dimension of that array, so we declared an array without dimension. Now we can determine the size of the array because we have obtained the parameter Number. So we use ReDim to redefine this array, dArray, with the size entered by the user, num.

  6. We use a for loop to create the random sequence. A system function Rnd is called to do that. We should first call the Randomize system function to initialize the random generator in Visual Basic, but here we skip that step to simplify this operation. To create a random sequence with certain bounds, you should use the following equation:

    (Upperbound − Lowerbound + 1) * Rnd + Lowerbound
    

    In our case, we want to create a random sequence between 0 and the amp, which is entered by the user when the program runs. So the upperbound is amp and the lowerbound is 0 in our application. Each newly created random data is assigned to the random array, dArray, based on the index.


Figure 2-37.


Now, click cmdCalc from the Object list to open the Calculate event procedure, and enter the code shown in Figure 2-38 into this procedure.

  1. Two local variables, ret and retValues, are declared at the beginning of this procedure. ret is used to retrieve the returned status of calling the DLL function, randCalc(), and it should be an integer in C/C++, so it is defined as Long type here in the Visual Basic domain. retValues is a user-defined data type, Result, which contains three statistical values for the random sequence.

  2. First we call the DLL function randCalc() with two arguments, the array size, num − 1, and an entire random data array (dArray) that was created in the Visual Basic domain. We need to mention two points about this function calling.

    • The size of the random sequence: The amount of the elements created in the random array is num − 1, not num. So when passing the size, we should pass num − 1.

    • The argument type of data array dArray: When we declared this DLL function, we defined this array type as a reference or a pointer, which means that this should be the address of the first element of the array to be passed into the DLL function. In Visual Basic, when we call this function, we should pass the first element of that data array to tell the DLL function developed in Visual C++ that this is the address of our passing array. The Visual C++ compiler will identify all following elements based on the address of this first element provided by Visual Basic. So here we passed the first element of our data array dArray(0) to the DLL function. You must pass this array as a reference and pass the first element of the array to the DLL function if you want to pass an entire array to the C/C++ domain from Visual Basic.

  3. After calling this DLL function, we need to check the returned status of calling this function. An error message will be displayed if any error is encountered for calling this function, and the program will terminate.

  4. We call another DLL function, randResult(), to retrieve the calculated results from the Visual C++ domain. retValues is a user-defined data structure.

  5. Finally, we assign the returned results to three textboxes to display the running results.


Figure 2-38.


Enter the End command to the cmdExit_Click() event procedure to exit the project.

Now, run the project and the GUI form, as shown in Figure 2-39, should be displayed on screen. Enter some numbers in the Amplitude and Number fields, such as 10 and 100, to define the magnitude and the number of the random array to be created. Click the Create button to create a new random data array in the Visual Basic domain. Then, click the Calculate button to call two DLL functions to calculate and return the statistical results for the random data array.


Figure 2-39.


The calculated results, which are maximum value, minimum value, and mean value of the random sequence, will be displayed in three textboxes, also shown in Figure 2-39.

You will notice that the calculated results have three significant digits, because we used a convert() function in the DLL function to translate the results to this mode.

Our first project is successful, and you have obtained hands-on experience in developing a DLL and interfacing that DLL from the Visual Basic domain. Click Exit to end the project.

2.3.4. A DLL Serial Port Test Project

A simple DLL project is discussed in the previous section. In this section, we show you a more complicated project in which a DLL is called and used as a drive to access and test the low-level hardware and serial ports from within the Visual Basic domain. To make things simple, we use an example DLL developed in Chapter 4, SerialDLL, which is used to interface with Smalltalk to test the serial ports. We need to make some modifications to that DLL file to match the requirements of our current application.

2.3.4.1. Develop a Visual C++ DLL Driver Project

We still want to use a single-loop test mode to test the serial port by sending a value to a port, then picking up that value from the same port to compare it with the sent-out value. The test is successful and the port is good if the sent-out value and the picked-up value are identical. To perform this test, you need to connect the sent-out pin (Pin-3 on a DB-9 port) and the receiving pin (Pin-2 on a DB-9 port) together. Refer to Figure 4-51 in Chapter 4 for more detailed information on this connection.

The functionality of this project is that all testing commands are sent from within the Visual Basic domain, and the associated command is passed to a DLL developed in Visual C++. The DLL also works as a device driver to test the serial port based on the command sent by Visual Basic. The commands sent from within Visual Basic are

  • Setup: Sends out a setup command to initialize a serial port.

  • Write: Sends out a value to a serial port.

  • Read: Picks up a value from a serial port.

  • Close: Terminates the DLL and Visual Basic application.

Make sure that you connect your serial port in the way shown in Figure 4-51 in Chapter 4. You should shut the power off when making the connection.

Now, let's take a look at the DLL developed in the Visual C++ domain. We want to modify an existing DLL project, SerialDLL, which is located in the Chapter 4VisualWorksvwdllSerialDLL folder. We don't need to develop a new project; instead, we can add this project to our new project and modify it inside our new project.

Launch Visual C++ 6.0 and create a new project with type Win32 Dynamic-Link Library, then enter VBSerialDLL in the Project name: field. Make sure that the Location: field contains our user-defined project folder, in our case, C:Vbdll. You can create and select any directory you like on your computer as a user-defined project folder. Click OK to open this new project.

Open Windows Explorer and copy the following files from the directory Chapter 4VisualWorksvwdllSerialDLL to our new project directory C:VbdllVBSerialDLL:

  • SerialDLL.cpp

  • SerialDLL.h

  • SerialDLL.def

Return to the Visual C++ domain, and create three new files as follows:

  • Source file: VBSerialDLL.cpp

  • Header file: VBSerialDLL.h

  • DEF file: VBSerialDLL.def

Open and copy the contents of each file to the associated newly created file. For example, open SerialDLL.cpp and copy its contents to the newly created source file VBSerialDLL.cpp. Do same thing for the SerialDLL.h and SerialDLL.def files. Now, open the copied header file, VBSerialDLL.h, and make the modifications highlighted with underscores as shown in Figure 2-40.

Figure 2-40.
							/****************************************************************************
							* NAME      : VBSerialDLL.h
							* DESC.     : Header file for VBSerialDLL.cpp
							* PGRMER.   : Y. Bai
							* DATE      : 7/2/2002
							*****************************************************************************/
							#ifndef _SERIALDLL_H_
							#define _SERIALDLL_H_
							#define MAX_STRING                256
							#define NOPARITY                    0
							#define ONESTOPBIT                  0
							#define RTS_CONTROL_DISABLE      0x00
							#define RTS_CONTROL_ENABLE       0x01
							#define DTR_CONTROL_DISABLE      0x00
							#define DTR_CONTROL_ENABLE       0x01
							#define msg(info) MessageBox(NULL, info, "", MB_OK)
							typedef struct
							{
							unsigned long ulCtrlerID;
							char       cEcho;
							char       cEORChar;
							long       lTimeout;
							long       lBaudRate;
							long       lDataBits;
							} SerialCreate, *pSerialCreate;
							typedef struct
							{
							char *pcBuffer;        /* added */
							int iMaxChars;         /* added */
							int piNumRcvd;        /* added */
							char cTermChar;       /* added */
							} CommPortClass;
							typedef enum
							{
							OK      = 0,           /* no error */
							EC_TIMEOUT,
							EC_FOPEN,
							EC_INVAL_CONFIG,
							EC_TIMEOUT_SET,
							EC_RECV_TIMEOUT,
							EC_EXIT_CODE,
							EC_WAIT_SINGLEOBJ,
							EC_INVALIDPORT,
							EC_WRITE_FAIL,
							EC_CREATE_THREAD,
							EC_UNKNOWNERROR
							}ERR_CODE;
							HANDLE    hPort;
							char*     sPortName;
							bool      PortCreateflg = false;
							/*local functions*/
							ERR_CODE PortInitialize(LPTSTR lpszPortName, pSerialCreate pCreate);
							ERR_CODE PortWrite(char* bByte, int NumByte);
							ERR_CODE PortRead(CommPortClass *hCommPort);
							void WINAPI ThreadFunc(void* hCommPorts);
							/*---------------------------------------------------------------------*/
							#define DllExport __declspec( dllexport )
							/*---------------------------------------------------------------------*/
							DllExport int CALLBACK Setup(int cPort, int bRate);
							DllExport int CALLBACK Read();
							DllExport int CALLBACK Write(int sdata);
							DllExport int CALLBACK Close();
							#endif
						

The only modification we made to this header file is adding the CALLBACK macro before each DLL function. This macro is very important in the connection between the DLL functions and the external applications. You would encounter a Bad calling convention error if you forgot to add this macro when you tried to call a DLL function from within the Visual Basic domain, and it is very difficult to debug this in your program.


Now, open the copied source file VBSerialDLL.cpp and make the following modifications, shown in Figure 2-41. The source code is relatively long, but don't worry about that. The purpose of showing this source file is to give you a complete picture of a derived project developed in the C/C++ domain. You will also find that the only modification we made to this source code is to add the CALLBACK macro, which has been underscored, before each DLL function implementation.

Figure 2-41.
							/***************************************************************************
							* NAME      : VBSerialDLL.cpp
							* DESC.     : DLL functions to interface to rs-232 serial port, called by VB.
							* PRGMER.   : Y. Bai
							* DATE      : 7/2/2002
							****************************************************************************/
							#include <stdio.h>
							#include <windows.h>
							#include <stdlib.h>
							#include <string.h>
							#include "VBSerialDLL.h"
							DllExport int CALLBACK Setup(int cPort, int bRate)
							{
							ERR_CODE rc = OK;
							pSerialCreate pParam;
							pParam = new SerialCreate;
							pParam->lBaudRate = bRate;
							pParam->lDataBits = 8;
							pParam->lTimeout = 3000;
							switch(cPort)
							{
							case 1:
							sPortName = "COM1";
							break;
							case 2:
							sPortName = "COM2";
							break;
							case 3:
							sPortName = "COM3";
							break;
							case 4:
							sPortName = "COM4";
							break;
							default:
							return EC_INVALIDPORT;
							}
							if (PortCreateflg)
							{
							msg("Port has been Setup ");
							return rc;
							}
							rc = PortInitialize(sPortName, pParam);
							if (rc != 0)
							msg("ERROR in PortInitialize()!");
							delete pParam;
							PortCreateflg = true;
							return rc;
							}
							DllExport int CALLBACK Write(int sdata)
							{
							int numByte = 1;
							char sByte[8];
							ERR_CODE rc = OK;
							sprintf(sByte, "%c", sdata);
							rc = PortWrite(sByte, numByte);
							if (rc != 0)
							msg("ERROR in PortWrite() !");
							return rc;
							}
							DllExport int CALLBACK Read()
							{
							int idata;
							char cdata[8];
							ERR_CODE rc = OK;
							CommPortClass* commClass;
							commClass = new CommPortClass;
							commClass->iMaxChars = 1;
							rc = PortRead(commClass);
							if (rc != 0)
							msg("ERROR in PortRead()! ");
							sprintf(cdata, "%d", commClass->pcBuffer[0]);
							idata = atoi(cdata);
							delete commClass;
							return idata;
							}
							DllExport int CALLBACK Close()
							{
							ERR_CODE rc = OK;
							if (PortCreateflg)
							CloseHandle(hPort);
							PortCreateflg = false;
							return rc;
							}
							ERR_CODE PortInitialize(LPTSTR lpszPortName, pSerialCreate pCreate)
							{
							DWORD          dwError;
							DCB            PortDCB;
							ERR_CODE       ecStatus = OK;
							COMMTIMEOUTS         CommTimeouts;
							unsigned char dBit;
							// check if the port has been created...
							if (PortCreateflg)
							{
							msg("Port has been initialized!");
							return ecStatus;
							}
							// Open the serial port.
							hPort = CreateFile(lpszPortName, // Pointer to the name of the port
							GENERIC_READ | GENERIC_WRITE, // Access (read/write) mode
							0,                       // Share mode
							NULL,                    // Pointer to the security attribute
							OPEN_EXISTING,           // How to open the serial port
							0,                       // Port attributes
							NULL);                   // Handle to port with attribute to copy
							// If it fails to open the port, return error.
							if ( hPort == INVALID_HANDLE_VALUE )
							{
							// Could not open the port.
							dwError = GetLastError();
							msg("Unable to open the port");
							CloseHandle(hPort);
							return EC_FOPEN;
							}
							PortCreateflg = TRUE;
							PortDCB.DCBlength = sizeof(DCB);
							// Get the default port setting information.
							GetCommState(hPort, &PortDCB);
							// Change the DCB structure settings.
							PortDCB.BaudRate = pCreate->lBaudRate;  // Current baud
							PortDCB.fBinary = TRUE;           // Binary mode; no EOF check
							PortDCB.fParity = TRUE;           // Enable parity checking.
							PortDCB.fOutxCtsFlow = FALSE;     // No CTS output flow control
							PortDCB.fOutxDsrFlow = FALSE;     // No DSR output flow control
							PortDCB.fDtrControl = DTR_CONTROL_ENABLE;// DTR flow control type
							PortDCB.fDsrSensitivity = FALSE;  // DSR sensitivity
							PortDCB.fTXContinueOnXoff = TRUE; // FALSE// XOFF continues Tx
							PortDCB.fOutX = FALSE;            // No XON/XOFF out flow control
							PortDCB.fInX = FALSE;             // No XON/XOFF in flow control
							PortDCB.fErrorChar = FALSE;       // Disable error replacement.
							PortDCB.fNull = FALSE;            // Disable null stripping.
							PortDCB.fRtsControl = RTS_CONTROL_ENABLE;// RTS flow control
							PortDCB.fAbortOnError = FALSE;    // Do not abort reads/writes on error.
							dBit = (unsigned char)pCreate->lDataBits;
							PortDCB.ByteSize = dBit;          // Number of bits/bytes, 4-8
							PortDCB.Parity = NOPARITY;        // 0-4=no,odd,even,mark,space
							PortDCB.StopBits = ONESTOPBIT;    // 0,1,2 = 1, 1.5, 2
							// Configure the port according to the specifications of the DCB structure.
							if (!SetCommState (hPort, &PortDCB))
							{
							// Could not create the read thread.
							dwError = GetLastError();
							msg("Unable to configure the serial port");
							return EC_INVAL_CONFIG;
							}
							// Retrieve the time-out parameters for all read and write operations on the port.
							GetCommTimeouts(hPort, &CommTimeouts);
							// Change the COMMTIMEOUTS structure settings.
							CommTimeouts.ReadIntervalTimeout = MAXDWORD;
							CommTimeouts.ReadTotalTimeoutMultiplier = 0;
							CommTimeouts.ReadTotalTimeoutConstant = 0;
							CommTimeouts.WriteTotalTimeoutMultiplier = 10;
							CommTimeouts.WriteTotalTimeoutConstant = 1000;
							// Set the time-out parameters for all read and write operations on the port.
							if (!SetCommTimeouts (hPort, &CommTimeouts))
							{
							// Could not create the read thread.
							dwError = GetLastError();
							msg("Unable to set the time-out parameters");
							return EC_TIMEOUT_SET;
							}
							EscapeCommFunction(hPort, SETDTR);
							EscapeCommFunction(hPort, SETRTS);
							return ecStatus;
							}
							/******************************************************************************
							* NAME        : PortWrite()
							* DESC.       : Write data to the port
							* DATE        : 11/9/00
							* PRGMER.     : Y. Bai
							******************************************************************************/
							ERR_CODE PortWrite(char* bByte, int NumByte)
							{
							DWORD dwError;
							DWORD dwNumBytesWritten;
							ERR_CODE ecStatus = OK;
							if (!WriteFile (hPort,      // Port handle
							bByte,      // Pointer to the data to write
							NumByte,    // Number of bytes to write
							&dwNumBytesWritten, // Pointer to the number of bytes written
							NULL))              // Must be NULL for Windows CE
							{
							// WriteFile failed. Report error.
							dwError = GetLastError ();
							msg("ERROR in PortWrite ..");
							return EC_WRITE_FAIL;
							}
							return ecStatus;
							}
							/*********************************************************************************
							* NAME      : PortRead()
							* PGRMER.   : Y. Bai
							*********************************************************************************/
							ERR_CODE PortRead(CommPortClass *hCommPort)
							{
							HANDLE hThread;                     // handler for port read thread
							DWORD IDThread;
							DWORD Ret, ExitCode;
							DWORD dTimeout = 5000;              // define time out value: 5 sec.
							ERR_CODE ecStatus = OK;
							if (!(hThread = CreateThread(NULL,  // no security attributes
							0,                         // use default stack size
							(LPTHREAD_START_ROUTINE) ThreadFunc,
							(LPVOID)hCommPort,         // parameter to thread funciton
							CREATE_SUSPENDED,         // creation flag - suspended
							&IDThread) ) )           // returns thread ID
							{
							msg("Create Read Thread failed");
							return EC_CREATE_THREAD;
							}
							ResumeThread(hThread);            // start thread now
							Ret = WaitForSingleObject(hThread, dTimeout);
							if (Ret == WAIT_OBJECT_0)
							{          // data received & process it...
							// Need do nothing because the data has been stored in the hCommPort in Thread Func.
							CloseHandle(hThread);
							}
							else if (Ret == WAIT_TIMEOUT)
							{
							// time out happened, warning & kill thread
							Ret = GetExitCodeThread(hThread, &ExitCode);
							msg("Time out happened in PortRead() ");
							if (ExitCode == STILL_ACTIVE)
							{
							TerminateThread(hThread, ExitCode);
							CloseHandle(hThread);
							return EC_RECV_TIMEOUT;
							}
							else
							{
							CloseHandle(hThread);
							msg("ERROR in GetExitCodeThread: != STILL_ACTIVE ");
							ecStatus = EC_EXIT_CODE;
							}
							}
							else
							{
							msg("ERROR in WaitFor SingleObject ");
							ecStatus = EC_WAIT_SINGLEOBJ;
							}
							return ecStatus;
							}
							/******************************************************************************
							* NAME      : ThreadFunc()
							* DESC.     : This function starts to read the port data.
							*           : The reason to use this thread is to overcome the hang up introduced by the
							*           : WaitCommEvent() function which waits for only the comm. event, not
							*           : synchronization event. The program will be dead-cycle without this thread.
							* PGRMER.   : Y. Bai
							* DATE      :  12/4/00
							*******************************************************************************/
							void WINAPI ThreadFunc(void* hCommPorts)
							{
							char Byte;
							BOOL bResult, fDone;
							int nTotRead = 0;
							DWORD dwCommModemStatus, dwBytesTransferred;
							CommPortClass* CommPorts;
							ERR_CODE ecStatus = OK;
							CommPorts = (CommPortClass* )hCommPorts;
							// Specify a set of events to be monitored for the port.
							SetCommMask(hPort, EV_RXCHAR|EV_CTS|EV_DSR|EV_RLSD|EV_RING);
							fDone = FALSE;
							while (!fDone)
							{
							// Wait for an event to occur for the port.
							WaitCommEvent(hPort, &dwCommModemStatus, 0);
							// Re-specify the set of events to be monitored for the port.
							SetCommMask(hPort, EV_RXCHAR|EV_CTS|EV_DSR|EV_RLSD| EV_RING);
							if (dwCommModemStatus & EV_RXCHAR||dwCommModemStatus & EV_RLSD)
							{
							// received the char_event, Loop for waiting for the data.
							do
							{
							// Read the data from the serial port.
							bResult = ReadFile(hPort, &Byte, 1, &dwBytesTransferred, 0);
							if (!bResult)
							{
							msg("ERROR in ReadFile !");
							fDone = TRUE;
							break;
							}
							else
							{   // Display the data read.
							if (dwBytesTransferred == 1)
							{
							if (Byte == 0x0D ||Byte == 0x0A) // null char or LF
							{
							CommPorts->piNumRcvd = nTotRead;
							fDone = TRUE;
							break;
							}
							CommPorts->pcBuffer[nTotRead] = Byte;
							nTotRead++;
							if (nTotRead == CommPorts->iMaxChars)
							{
							fDone = TRUE;
							break;
							}
							}
							else
							{
							if (Byte == 0x0D ||Byte == 0x0A) // null char
							{
							msg("Received null character ");
							fDone = TRUE;
							break;
							}
							}
							}
							}while (dwBytesTransferred == 1); //while (nTotRead < pRecv->iMaxChars);
							}
							}  // while
							return;
							}
						

Besides the DLL functions, there are 4 additional local functions implemented in this project. The purpose of these local functions is to access the low-level device driver to communicate with the serial port to be tested.

For this DLL, we assume that we have 4 COM ports available, ranging from COM1 to COM4. This is a very popular COM port arrangement for most computers. Generally, most users can access COM1 or COM2 on their computers, and some of the other ports are utilized by the system itself.

In our application, COM1 is available to us, and we will use this port as our testing port. Open the copied DEF file VBSerialDLL.def, and make the modifications shown in Figure 2-42.

Figure 2-42.
							LIBRARY VBSerialDLL.dll
							EXPORTS
							Setup
							Read
							Write
							Close
						

Now, click Build|Build VBSerialDLL.dll from the Visual C++ 6.0 menu bar to build the DLL project. If everything is fine, open Windows Explorer and copy this DLL file, VBSerialDLL.dll, to our user-defined DLL directory C:stdll.

2.3.4.2. Develop a Visual Basic Testing Project

Let's switch to the Visual Basic domain to develop our Visual Basic project to call that DLL to test the serial port. In this Visual Basic project, we plan to create two forms; one is used to get the user's inputs for the COM port setup parameters, COM port number and baud rate, and the other is used to send commands to test the serial port.

Launch Visual Basic 6.0 and create a new Standard EXE project. In the opened project, click Form1 on the screen and enter the following properties in the Properties window:

  • Name: VBSerialDLL

  • Caption: VB Serial Port Test DLL

Click the Project1 icon on the Project Explorer window, and enter VBSerial in the Name property of this project to change the project's name to VBSerial.vbp.

Save the form now named VBSerialDLL.frm to our user-defined project folder. Also, save this project (VBSerial.vbp) as VBProject to our user-defined project folder C:Vbdll.

Now, click Project|Add Form. In the opened dialog box, keep the default New Form selected, and click Open to open this new Form.

Click on the new form and enter Setup in the Name property of this form to name this form Setup. Add the following components to this form:

ComponentNameCaption
LabelLabel1Enter Setup Parameters for Serial Port
LabelLabel2Port Number
LabelLabel3Baud Rate
TextboxtxtPort 
TextboxtxtBaudRate 
Command buttoncmdOKOK

Set the Form's BackColor property to Yellow. Your finished Form, Setup, should match the one that is shown in Figure 2-43.


Figure 2-43.


Now we have two forms in this project. We need to tell the Visual Basic interpreter which form should be displayed first as the project runs. To do that, click the Project|VBProject Properties. . . menu item from the menu bar, the last item on the Project submenu, to open the Project Properties dialog box. Select Setup from the Startup Object: field to make the Setup Form be our first form to be displayed as the program runs. Click OK to save this setting and close the dialog box.

Now, return to the Visual Basic workspace, click on another Form, VBSerialDLL, and add the following components to that form:

ComponentNameCaption
LabelLabel1Welcome to The Serial Port Testing DLL Project
FrameFrame1Port Setup
LabelLabel2COM Port
LabelLabel3Baud Rate
LabellblComPort[*]
LabellblBaudRate[*]
FrameFrame2Testing Data
LabelLabel4Sending Data
LabelLabel5Reading Data
Text BoxtxtWdata
Text BoxtxtRdata
Command buttoncmdSetupSetup
Command buttoncmdTestTest
Command buttoncmdExitExit
TimerVBSerialTimer[**]

[*] BackColor: Yellow

[**] Interval: 500 ms

Please note that we add a Timer to this project. The purpose of this Timer is to periodically send out the test data to the serial port, and read back the test data from the same serial port after the data is sent out. Then we can compare the sending-out data and the reading-back data to check whether both data are identical. In the ideal case, these two data should be the same if the serial port is fine and communication between the program and driver has no problem.

The Timer has one property, Interval, which is used to determine how often the Timer should be activated and to execute the Timer event procedure to perform some desired job inside the procedure. You should place the task code inside the Timer event procedure and let the Timer handle that job periodically for you. The unit of the Interval is ms. Your finished Form, VBSerialDLL, should match that shown in Figure 2-44.


Figure 2-44.


Now let's develop code for our project. Click the Setup icon from the Project Explorer window to open the Setup Form. Then click the View Code button to open the code window for this Form.

First, select the General item from the Object list to open the Form's General Declaration section, and enter the code Option Explicit at this section. Remember, the purpose of this statement is to avoid the misuse of variables in this program as the Form runs.

Next, select the Form item from the Object list to open the Form_Load event procedure. This procedure is the first procedure as this Form is executed. We want to display this Form at a certain location on the screen when the program executes. So we use the Object Me and its properties to set up the initial position for this Form. The Me object is the Form itself and the properties we want to use are Left and Top, which can be considered as the upper-left coordinates of the Form on screen.

Enter the code shown in Figure 2-45 into the Form_Load event procedure.

  1. This is the statement we entered at the Form's General Declaration section as noted earlier.

  2. Me.Left is the left-most coordinate of the Form on screen. Coordinate units are in pixels. We set the upper-left corner coordinates of the Form at x = 2500 and y = 2500 pixels.


Figure 2-45.


Before we can continue with the following coding, we first need to create a Code Module to hold our two global variables, comPort and baudRate. Because we created two Forms in this project, the Setup Form and VBSerialDLL Form, these two Forms need to share some variables to communicate to each other. So we need to create two global variables to hold the values of the port number and the baud rate, which are obtained from the Setup Form and entered by the user as the program runs. The VBSerialDLL Form also needs the values of these two variables to initialize the serial port.

Click Project|Add Module from the menu bar to open the Add Module dialog box. Keep the default Module selected, and click the Open button to open that Module. You will find that a new Module has been added to your project by checking the icons in the Project Explorer window. Right-click on that newly added Module, Module1, and select Save Module1 As. . . to open a dialog box. In the opened dialog box, browse to our user-defined project folder, C:vbdll, change Module1's name to VBSerial, and click Save to save this Module.

Double-click this newly created Module, VBSerial.bas, from the Project Explorer window to open it, and add the code shown in Figure 2-46 into this Module.


Figure 2-46.


Two global variables, comPort and baudRate, are created here. Please note that if you want to create a global variable, use Public instead of Dim and place it in front of the variable. Because these two variables are both integer types in C/C++ and will be passed into the C/C++ domain, a Long data type is implemented for them.

Now, we need to write code for the OK command button. Select cmdOK from the Object list to open its event procedure. Enter the code shown in Figure 2-47 into this event procedure.

  1. When the OK command button is clicked by the user, the program first needs to check two textboxes to confirm that the user entered valid values for the two parameters COM Port and Baud Rate. A message box with an error message will be displayed on screen if no value was entered to remind the user to enter a valid value.

  2. Also, we need to check the values of COM Port and Baud Rate entered by the user to confirm that the values are within a range of valid parameters. Another message box with an error message will be displayed on screen if any value is out of range.

  3. If both values are valid, we need to reserve them to the global variables; COM Port will be assigned to the comPort global variable, and Baud Rate will be assigned to the baudRate variable. These two variables will be shared by the VBSerialDLL Form later on.

  4. Finally, we need to hide the Setup Form and display the VBSerialDLL Form, because the Setup Form has finished its mission and we need to use the VBSerialDLL Form to talk to DLL to test the serial port. You can also use Unload to remove the Setup Form because this Form will no longer be used once we obtain two parameters from the user.


Figure 2-47.


Now, click the VBSerialDLL icon from the Project Explorer window to open the second Form, VBSerialDLL. Click the View Code button to open its code window. First, we need to declare DLL functions that will be used in this Form and some of the Form's level variables, which are shown in Figure 2-48.

  1. In the Form's General Declaration section, we declare a Boolean variable, sTest, which will be used as a flag or a monitor to hold the status of the current project. This flag should be reset (False) at the beginning of the project and should be set (True) if the Test command button is clicked by the user, meaning that the project is under testing status.

  2. Following the declaration of the Form's level variable, four DLL functions are declared. All DLL functions used in this project are located at a DLL named VBSerialDLL.dll, which is developed in the Visual C++ domain. The first function is Setup(), which is used to initialize the serial port with two major parameters, port number and baud rate. Both parameters are integer values in the Visual C++ domain, so we pass them by value, and the data type is mapped to Long in the Visual Basic domain. This function returns an integer value (in the C/C++ domain) to provide feedback on the status of executing that function to the system. A mapping of data type Long is applied to this returned variable, too.

    The second function is Write(), which is used to pass an integer to the DLL function, and the latter will send this data to the serial port. In reality it is sending the integer to the buffer of the Transmitting terminal of the serial port. The argument wData is mapped to Long in Visual Basic. One point we want to emphasize is the function name. Because Write is a reserved keyword in Visual Basic, and a system function with the same name has been defined in Visual Basic, we cannot use the same name to declare our user's function. So an Alias clause is used here to indicate that our real function name is Write. But in order to avoid conflict with the system function, a pseudo-name, sWrite, is used to represent that real function in the program. An error message “Expected Identifier” will be displayed if you insist on using the real function name in this declaration. This function also returns an integer as the feedback to the system to show the executing result of this function call, and a 0 means successful.

    The next DLL function is Read(), which is used to read back the data that is sent out by the program. Similarly to the Write() function, Read is also a reserved keyword in Visual Basic, so an Alias clause is utilized here, too. You need to call the sRead() function in the program to access this Read() function to pick up the data from the serial port. A running status of this function will be returned from the calling function to the system to show the result of executing that function. A returned 0 means successful.

    The final DLL function is Close(), which is used to clean up the environment setup for testing the serial port, including closing the reading thread. This function also needs an Alias clause because Close is a reserved keyword in Visual Basic. A returned value is used to show the running result of this function. A 0 means successful.

    Some readers may have found that the DLLs' names don't have the extension (.dll). That is okay if you have copied that DLL file to a searchable or a system DLL directory on your machine. Computers can automatically recognize and locate your DLL with no problem.


Figure 2-48.


While still in the code window, click the Form item from the Object list to open the Form_Load event procedure, and enter the code shown in Figure 2-49 into this procedure.

  1. This procedure will be loaded first and executed when this Form is executed. Therefore, any initialization job should be performed here. We still use the Me object (the Form itself) and its position properties (Left and Top) to set up the initial location of this Form window on the screen as it is displayed on the system. Here both x and y coordinates are 2000 pixels.

    Another initialization job is to reset the flag, sTest, to indicate to the system that the current status of the program is in idle.

  2. Next, we need to display the current settings of the serial port with two parameters, comPort and baudRate. The values of those two global variables are obtained from the first Setup Form and entered by the user. Later on we need these two parameters to initialize the serial port by calling a Setup() DLL function. We assign these two parameters to two labels to display them on the Form.


Figure 2-49.


Now, we need to write code for the Setup command button event procedure. The functionality of this event procedure is to call a DLL function to initialize the serial port with two parameters, comPort and baudRate. Also, the procedure needs to check the returned status of that DLL function to make sure that the function is executed successfully. An error message should be displayed on screen to warn the user if this function encountered any mistake and if the returned value was not equal to zero.

Click the cmdSetup item from the Object list to open the Setup event procedure, and enter the code shown in Figure 2-50.

  1. A local variable ret is declared at the beginning of this procedure, which is used to hold and reserve the returned status of executing the DLL function Setup(). Then the DLL function Setup() is called with two global variables to initialize the serial port.

  2. When the function returns, we need to check its returned status to make sure of the correctness of executing that function. A 0 should be returned if this calling is successful; otherwise, an error message should be displayed on screen and the program should exit the event procedure.


Figure 2-50.


The coding for the Test command button event procedure is very simple; that is, when the Test command button is clicked by the user, the flag sTest should be set, and this will tell the system that the current status of the program is testing. Click cmdTest from the Object list to open its event procedure and enter the code shown in Figure 2-51.


Figure 2-51.


This flag is set to inform the Timer, VBSerialTimer, which was added when we built this Form. You will see that later from the Time event procedure. The Timer will check this flag periodically, and the check result will be used to determine what to do for the next step in the Timer event procedure.

Now, let's take a look at the Timer event procedure by clicking the VBSerialTimer item from the Object list to open its event procedure. Enter the code shown in Figure 2-52 into this event procedure.

  1. Two local variables, ret and randNum, are declared at the beginning of this procedure. The first variable will be used to hold and reserve the returned status of calling a DLL function, and the second variable is used to hold a random data that is created by calling the Rnd function defined in Visual Basic. Both variables are integers in C/C++, so a Long type is used here to map them to integers in the C/C++ domain.

  2. We want to use a sequence of random numbers to test the serial port, so we create this random sequence by calling the Rnd system function. The Rnd will create a random number in a double type, and therefore we need to use an Int to cast it and convert it to an integer value. The range of this random data is between 0 and 100. Of course, you can choose a different range if you like.

  3. After random data is created, the Timer needs to check the current status of the program by inspecting the sTest flag. As we mentioned earlier, this flag should be set if the user has clicked the Test command button, to indicate that the current program is in testing status.

    If this flag has been set, the Timer will begin to execute the test task. First, the random number is sent to the txtWdata textbox. Then, the DLL function sWrite() is called and the random number is passed into the DLL, which finally will be sent to the serial port by a driver function in the DLL.

  4. The Timer will check the status of executing that DLL function by inspecting the returned value ret. A 0 should be returned if the function call is successful; otherwise, an error message will be displayed on screen and the program will exit the procedure.

  5. If everything is normal, the Timer will call the sRead() DLL function to read back the random data sent to the serial port. Also, the returned data will be displayed in the txtRdata textbox to give the user direct feedback.

  6. Next, the Timer needs to check the testing result by comparing the sent-out data and the read-back data. The testing would be successful if both data are identical; otherwise, the testing has failed and an error message will be displayed on screen, then the program will exit the procedure.


Figure 2-52.


Finally, we need to write code for the Exit command button event procedure. Click cmdExit from the Object list to open its event procedure. Enter the code shown in Figure 2-53 into this procedure.

  1. A local variable ret is declared first. Then the program's flag, sTest, is reset. This is a very important step. Without this step, the flag would be still in set status, and the Timer would continue to call DLL functions to send out and read back the data from the serial port. In other words, the Timer event procedure will continue to run even after the user clicks the Exit command button and wants to stop the testing program. After this flag is reset, the Timer will be informed that the testing is done and the Timer event procedure should be terminated.

  2. After the Timer event procedure is terminated, an sClose() DLL function is called to clean up the setup environment during the testing process in the Visual C++ domain. Similarly to other DLL function calls, a check of the returned status will be executed to make sure that the function call is successful.

  3. To end the program, an End command is needed.


Figure 2-53.


At this point, we have finished developing a Visual Basic test project. Now let's test our project by pressing the F5 key from the keyboard. As your project runs, the first Form, Setup Form, is displayed on screen. Enter the setup parameters, comPort and baudRate, in the two textboxes, as shown in Figure 2-54.


Figure 2-54.


In our application, the available port is COM1 and the baud rate we selected is 9600. You can select different parameters based on your system. Click the OK button to continue the project.

The second Form in our project, VBSerialDLL, is displayed on screen, as shown in Figure 2-55. You will find that the two parameters we entered in the last Form are displayed in the Port Setup information box. Click the Setup command button to call the DLL function Setup() to initialize the serial port with these two parameters. Then click the Test button to begin testing.


Figure 2-55.


Random data will be created and sent to the serial port. That data is also displayed in the Sending Data textbox. The read-back data will be displayed in the bottom textbox labeled Reading Data. The interval between displaying these two data is about 500 ms or maybe a little longer. You should find that the sending-out data is identical to the reading-back data, which means that the serial port works fine and our testing is successful. The running result is shown in Figure 2-56.


Figure 2-56.


Occasionally, an error message may be displayed to indicate that the testing for the serial port was not successful. This is understandable because a chance for error in the serial port application exists in the real world. Also, sometimes an error message doesn't mean that the error comes from the serial port itself; it may be introduced by some disturbance or magnetic field around the computer system. The chance of that kind of error, however, is very slight.

All files of this project, including DLL source files, header file, DEF file and DLL file, Visual Basic source files, Code Module, and executable file, are on the accompanying CD-ROM in the Chapter 2VBSerialDLL folder. You can copy these files onto your local machine and run them. You should copy the DLL file to a searchable folder on your system in order to run this project on your machine.

2.3.5. Call a DLL to Interface Multiple Tasks in Visual C++ 6.0

2.3.5.1. Functionality of the Multiple Tasks DLL Project

In this section, we want to show readers how to interface multiple tasks in the Visual C++ domain from within Visual Basic. To make things simple, we will develop a Visual C++ project that includes two threads. These two threads will run simultaneously in the Visual C++ domain, and a Visual Basic project will interface these two threads to perform a loop data collecting sequence. The communication or synchronization between two threads is handled by events, by either SetEvent() or ResetEvent() functions. The functional block diagram of this project is shown in Figure 2-57.


Figure 2-57.


Two DLL functions work as interface functions to set up associated events based on the command sent from within Visual Basic. A vbSendEvent event will be set if the Send Data command is received by the DLL function SendData(); otherwise, the vbReadEvent event will be set if the Read Data command is received by the ReadData() DLL function.

The two threads work separately and communicate to each other by using events. A WaitForMultipleObjects() system function is placed in a While loop for each thread. This function waits for multiple objects to happen, including Exit, vbSendEvent, and vbReadEvent events. Thread I waits until either the Exit or vbSendEvent is signaled. If the Exit event is signaled, the program will exit the While loop and terminate Thread I. If the vbSendEvent is signaled, the program will call another wait function, WaitForSingleObject(), to wait until the ReadEvent is signaled (this event will be set by Thread II), which means that Thread II has finished picking up a data from the shared data buff (sdata) and is ready to pick up another data. When this situation occurs, Thread I will transfer the data sent out by Visual Basic to a shared data buff, sdata, and then set the event, SendEvent, to inform Thread II that a new data has been received from Visual Basic, and it has been stored into the shared data buff. Thread II can go ahead to pick it up. Then Thread I will continue to return to the While loop until another event happens. This loop runs infinitely until some event is signaled.

Similarly to Thread I, Thread II has an infinite While loop. A WaitForMultipleObjects() function waits for any event to happen, including Exit or vbReadEvent, which is signaled by the DLL function ReadData() when a Read Data command is sent out from Visual Basic and received by that DLL function. As this event is signaled, Thread II calls another wait function, WaitForSingleObject(), to wait until the SendEvent event is signaled, which should be signaled by Thread I when a data is ready. As this situation occurs, Thread II picks up the data from the shared data buff (sdata) and assigns this data to another data buff, rdata. Then Thread II will set ReadEvent to inform Thread I that Thread II has finished picking up that data, and it is ready to pick up the next one. If an Exit event is received, the program will exit the While loop and terminate Thread II. Otherwise, Thread II will continue to work in this infinite loop until some event happens.

In this way two threads work separately but communicate to each other by using the events, and interface with Visual Basic by using other events.

2.3.5.2. Develop a Multiple Tasks DLL in the Visual C++ Domain

Based on the working principle of this DLL, we will now open a Visual C++ 6.0 workspace and create a new project with type Win32 Dynamic-Link Library. Enter VBMultTask in the Project_name: field, and click OK to open this project.

Click File|New to create a new header file named VBMultTask.h, and enter the code shown in Figure 2-58 into this new header file.

  1. Four global variables are declared and defined at the beginning of this header file. The variables sdata and rdata are two shared data buffers, which are used to store the sent-out and read-back data for the Visual Basic domain. Because we want to test a multiple-task system, we cannot directly assign the sent-out data coming from the Visual Basic program to the read-back data to Visual Basic. We need to use these two shared data buffers to transfer the data from one thread (Thread I) to another thread (Thread II) based on the events that synchronize these two threads.

    The vbSendData and vbReadData are two temporary data buffers that are used to reserve the sent-out data and read-back data from the Visual Basic domain.

  2. Two thread handlers and thread IDs are needed for this project, and each handler and ID is associated with each thread. A handler will be returned if a thread is created successfully; this handler will be used as a unique ID for this newly created thread. Because the WaitForMultipleObjects() system function can handle multiple objects, such as an event, a process, or even a thread, we can use this function to monitor the status of those objects until some objects are signaled. The thread object is represented by its handler when it is waiting or monitoring in the program. A timeout value and some other global variables are also declared here.

  3. Besides the thread handler, five (5) other handlers are also declared. These handlers are all event handlers. The first one is the Exit event handler, which is used to inform the system to terminate and exit the running status. This event has the highest priority in this project. As soon as this event is received (when the Exit button in the Visual Basic domain is clicked by the user, this event handler is signaled), the system immediately stops executing the current command, exits the thread, and finally exits the program.

    Following the Exit event handler, two other event handlers, vbSendEvent and vbReadEvent, are declared. These two events are related to the two situations when the project runs. The event vbSendEvent will be set when Visual Basic sends out a data to the DLL and the latter receives that data. This event will be used to inform the system function WaitForMultipleObjects() in Thread I that a data has been sent out by Visual Basic and that data has been received by the DLL. As Thread I receives this event, it will check the local read data event (ReadEvent) to see if Thread II is ready to receive this new data.

    The vbReadEvent will also be signaled when Visual Basic issues a command to indicate that Visual Basic is ready to pick up data from the DLL. As soon as the DLL receives this command, the vbReadEvent event will be set, which is used to inform the system function WaitForMultipleObjects() in Thread II that Visual Basic is ready to pick up a data. When Thread II receives this event, it will check the local send data event (SendEvent) to see if Thread I is ready to send a new data. Two other events, SendEvent and ReadEvent, as previously mentioned, are used to coordinate the translation between two threads for the desired data.

  4. Two thread functions are declared here, and these two thread functions are real processes to handle the thread jobs. Because currently we don't need to pass any parameter from the DLL functions to thread functions, a NULL pointer is used here (void* num).

  5. Finally, four (4) DLL interface functions are declared.


Figure 2-58.


Click File|New again to create a new C++ Source File named VBMultTask.cpp, and enter the code shown in Figure 2-59 into this new source file.

  1. First, let's take a look at the initialization DLL function Init(). The temporary data buffers, vbSendData and vbReadData, are initialized to zero. Then five (5) events are created by calling the CreateEvent() system function. The first argument is an event attribute, which is used to indicate whether the newly created event object can be inherited by a child process. If a NULL is used for this parameter, this event cannot be inherited by any child process. In our case, we don't have a child process, so a NULL is used here. The second argument is a flag that is used to indicate whether the status of this event can be reset automatically or manually after it is signaled. A FALSE means that this event can be reset (nonsignaled) automatically by the system when it is responded to; a TRUE means that you have to call ResetEvent() to reset this event after it is responded to in the program. You will find that for most events, this argument is set to FALSE, which means that we want the system to reset the event after the event is responded to. The only exception is the Exit event, for which the second argument is set to TRUE. This means that we have to reset that event manually when it is signaled. This is a safe way to handle this event because when this Exit event happens, we need to make sure to clean up all event handlers, including the Exit event handler. To explicitly call a ResetEvent() system function after performing this cleaning job provides a good guarantee that our program will exit the system properly.

    The third argument is the initial status of the event, which means that this event is signaled or reset when it is created. For the first four events, this argument is FALSE, which indicates that all those events are set to nonsignaled status and they need to be signaled by calling the SetEvent() system function in the program. But for the fifth event, which is a local event ReadEvent, this argument is set to TRUE, which means that this event is signaled and is ready to be used. The reason is that for the ReadEvent, you should set its initial status as true and make it ready to receive a new data. In this way, the first time Thread I checks the status of the event ReadEvent, Thread I can receive a signaled status and continue to send a datum to the shared buffer to keep the threads running in a normal mode. Otherwise, Thread I will be in dead cycle waiting for the ReadEvent to be set.

    The final argument is the name of the created event, which is a null-terminated string and is used to represent a name for the event. In most cases, we don't need a name for the event (a NULL can be used) except for the multiple tasks working in a shared memory mode, in which case because the events are running at different processes, a name is needed to identify what is what for the different processes.

  2. The first thread is created by calling the CreateThread() system function. This function has six (6) arguments. The third argument is used to indicate the name of the thread function, which is the main part of the thread to handle the thread job. In our case, it is named ThrdFunc1. You can use any legal name for this item. The fifth argument is a flag that is used to tell the system if this thread should run immediately or suspend when it is created. For our application, we prefer to use the suspending flag.

    If this CreateThread() function is successful, it will return a thread handle. Otherwise, a NULL will be returned to indicate that this function call failed. In our case, we need to create two threads; a handle array is used here, and the first thread handle is returned and reserved at hThread[0]. An error message will be displayed on screen if this function is unsuccessful.

  3. We begin to run the thread function ThrdFunc1 by calling a system function, ResumeThread(), with the returned thread handle hThread[0] as the argument.

  4. Similarly, we create and name the second thread ThrdFunc2. The returned handle of the second thread is reserved in hThread[1] if this creation is successful.

  5. Calling ResumeThread() with the argument hThread[1] runs the second thread function. Return a 0 to tell Visual Basic that no error was encountered for this DLL function call. At this point, two thread functions have already run and two While loops are in process to check and wait for any event.

  6. Now, let's take care of the DLL functions. The first DLL function is SendData(). This function has one argument, sData, which is the data sent out by the Visual Basic domain. As the system receives this function call from Visual Basic, the system will reserve the received data in the temporary data buffer, vbSendData, and signal the vbSendEvent event by calling the Set Event() system function. As this vbSendEvent event is signaled, the WaitForMultipleObjects() function in the While loop in Thread I will be activated because it received an event. Then the system can continue to check the status of the local event ReadEvent to determine whether to send a new data to the shared data buffer. This function returns a 0 to Visual Basic to indicate that this function call is successful.

  7. The ReadData() DLL function works similarly to the SendData() function as discussed. As the system receives this function call from Visual Basic, first the system sets the event vbReadEvent to inform the system function, WaitForMultipleObjects(), in Thread II that Visual Basic is ready to pick up a new data from the DLL—from the shared data buffer rdata, to be precise. Then the system delays for 2 ms to give enough time for the threads to finish data translation. After that, the data is stable and ready to be picked up. An assignment is executed to translate data from the shared data buffer rdata to the temporary reading buffer vbReadData. The latter will be returned to the Visual Basic domain as a feedback to this function call.

  8. The last DLL function is Exit, which is a very important function in this project. Because we created quite a few different objects and components in this project, including the events and threads, all these components should be cleaned up when the project is terminated. Cleanup includes closing all event handlers and all thread handlers. The challenging job is to terminate a running thread. A system error would be displayed if the thread was not terminated properly. Fortunately, we utilized the system function WaitForMultipleObjects(), which provides us a convenient way to end a thread and not create a dead cycle for our program. During the waiting status, the WaitForMultipleObjects() will return control to the system CPU and allow the CPU to handle some other more important jobs if no other event is signaled in the current program. In this way, the computer can run in a real multiple-task environment to handle different processes.

    When the system receives this Exit function call from the Visual Basic domain, the system immediately sets the event ExitEvent to activate the system function, WaitForMultipleObjects(), from two threads. The system function will execute a break command to exit the While loop, and furthermore to exit the threads. The system will delay about 2 ms to allow all threads to terminate. Then the system calls the WaitForMultipleObjects() function to make sure that all threads have been absolutely terminated. This function has four (4) arguments. The first one is used to indicate how many objects the function needs to wait for. In our case, we only need to wait for two threads, so 2 is used here. The second argument is an object array, which holds all objects that need to wait. Here a pointer hThread is used for this array (array is equivalent to a pointer). The third argument is a flag that is used to indicate whether any or all events need to be signaled. A TRUE means that all events need to be signaled. So a TRUE is used here to indicate that the function will wait until both thread objects are signaled, which means that both threads are terminated and returned. The fourth argument is a timeout value. Here we used an INFINITE to ask the function to wait forever until both threads are terminated.

    After both threads are terminated and returned, we need to manually reset the ExitEvent event as we previously discussed.

    After resetting ExitEvent, we need to clean up the environment by closing all objects' handlers, which includes all event handlers and thread handlers. Finally, a 0 is returned to Visual Basic to indicate that this function call is successful.

  9. Now let's take a look at the thread functions. In the first thread function, ThrdFunc1, an event array hEvent[] is created at the beginning of the function. Two objects are included in this array, ExitEvent and vbSendEvent. The first one is activated by the Exit command and the second one is activated by the SendData() DLL function we mentioned previously.

  10. An infinite loop starts after this initialization. Inside this infinite loop, a WaitForMultipleObjects() system function is used to wait for one of two events to happen. This function has four (4) arguments. The prototype and the functionality of this function have been described in step H. We only want to mention one point, which involves the third argument. In this application, the third argument is set to FALSE, unlike the TRUE setting used in step H. A FALSE flag means that the system function WaitForMultipleObjects() will be activated by one of two events, either ExitEvent or vbSendEvent, but not by both events. So either event will make this system function activate.

    If this system function is activated by the first event (WAIT_OBJECT_0), which means that the ExitEvent is signaled, the program will break from the While loop, exit, and terminate Thread I.

  11. Otherwise, the system function will be activated by the second event (WAIT_OBJECT_0 + 1), which means that vbSendEvent is signaled. In that case, the system will call another function, WaitForSingleObject(), to monitor and wait for another event, ReadEvent, to be signaled. This local event should be signaled by Thread II when Visual Basic is ready to pick up another data from the DLL function. If Thread II finished processing a data and it is ready to pick up another data, this event will be set after that. As the system function WaitForSingleObject() in Thread I is activated by this event, Thread I will translate the data received from Visual Basic from the temporary writing buffer vbSendData to the shared data buffer sdata, then set the local event SendEvent to inform the WaitForSingleObject() function in Thread II that the new data is ready to be picked up. A 1 ms delay is executed by calling a Sleep() function for each While loop.

  12. Basically, the second thread function ThrdFunc2 is similar to the first one. One difference is the event array. The second event in this array is vbReadEvent, and this event will be set by the DLL function ReadData() when Visual Basic is ready to pick up another data from the DLL function.

  13. If an Exit event is signaled, same as in the first thread function, the system will break from the While loop, exit, and terminate Thread II.

  14. If a vbReadEvent event is signaled, the system will call the WaitForSingleObject() function to monitor and wait for the local event SendEvent, which will be set by Thread I when a new data is received from Visual Basic. If a new data is received and the local event SendEvent is signaled, the system will translate the new data from the shared data buffer sdata to rdata, and set event ReadEvent to inform the system function WaitForSingleObject() in Thread I that the data has been picked up and Thread II is ready to pick up the next data. A 1 ms delay is executed by calling a Sleep() function for each While loop.


Figure 2-59.


Before we can build our project, we need to create a DEF file to export our DLL functions. In the Visual C++ 6.0 workspace, create a new text file named VBMultTask.txt. Then click Project|Add To Project|Files. . . to open a dialog box. In the opened dialog box, find the newly created text file VBMultTask.txt and right-click it, select Rename from the pop-up menu, then rename the extension of this file to VBMultTask.def. Select this renamed file and click OK to add this file into our project. You can delete the original text file VBMultTask.txt by first selecting that file, and then clicking Edit|Delete from the menu bar.

Now, double-click the VBMultTask.def file to open it and enter the code shown in Figure 2-60 into this file.

Figure 2-60.
							LIBRARY VBMultTask.dll
							EXPORTS
							Init
							SendData
							ReadData
							Exit
						

Now click Build|Build VBMultTask.dll to build the project. Copy the resulting DLL file VBMultTask.dll to the user-defined dll directory C:stdll. At this point, we have finished developing a multiple-task DLL project.

2.3.5.3. Develop a Visual Basic Project to Call the Multiple Tasks DLL

The purpose of developing this Visual Basic project is to interface the DLL functions we developed earlier to test a multiple-task DLL project from within Visual Basic. This test project also confirms that Visual Basic is an Object-Oriented-Event-Driven-Programming language and it is useful for a multiple-task project. It is an especially good candidate to work in a multiple-task environment.

Open Visual Basic 6.0 and create a new Standard EXE project. Change the Form name from the default Form1 to VBMultTask.frm, and change the Project name from Project1 to VBMult.vbp. Save the Project and Form to our user-defined project directory, C:vbdll.

Add the following components to the new Form, VBMultTask.vbp; your resulting screen should look like that in Figure 2-61.


Figure 2-61.


ComponentNameCaption
LabelLabel1VB Multiple Tasks DLL Testing
LabelLabel2Loop Number
TextboxtxtLoop 
FrameFrame1Data Testing
LabelLabel3Sending Data
LabelLable4Reading Data
TextboxtxtSend 
TextboxtxtRead 
Command buttoncmdInitInitialize
Command buttoncmdSendSend
Command buttoncmdReadRead
Command buttoncmdLoopLoop
Command buttoncmdExitExit
Form TitleVBMultTaskVB Multiple-Task DLL Test

The purpose of the txtSend and txtRead textboxes is to display the sent-out and read-back data. The txtLoop textbox is used to receive the number of the loops when you want to perform a loop testing in this project. The purposes of the five command buttons are straightforward, and you have to first click the Initialize button to start this project.

Click the View Object button on the Project Explorer window to open the code window. Click the General item from the Object list to open the Form's General Declaration section, then enter the statement Option Explicit into this section.

Click Add-Ins from the menu bar to open the API Viewer, click File|Load Database File. . ., then browse and select the Win32 API database WIN32API.MDB from the opened dialog box to load this database to our project. Browse and select the Sleep API function from the Available Items list, and click Add to add this function to the Selected Items list box on the bottom of this Viewer. Make sure that the Private radio button is selected before you click the Add button to add this function. Click the Insert button to add this function to the project, inserting it into the Form's General Declaration section. Click Yes in the message box to confirm that we are sure we want to add this function to the project. Now, return to the code window and open the Form's General Declaration section by clicking the General item from the Object list, and enter the code shown in Figure 2-62 into this section.

  1. In this Visual Basic project, we first need to create some Form's level variables. The randNum is used to store a created random number in the project, and the initFlg, which is a Boolean variable, is used to hold the initialization status. This flag will be set if the initialization DLL function Init() has been called and the initialization has been completed.

  2. A sequence of DLL functions is declared in this section. All DLL functions are located in a DLL named VBMultTask.dll that is developed in the Visual C++ domain. Only the SendData() function has an argument, sdata, which is passed by value with a data type of Long. All functions need to return a value to indicate whether this function call is successful. An Alias name is used for the Exit() function because the Exit is a reserved keyword in Visual Basic. You can only use the sExit to call the Exit() function in the program.

  3. Also, you will find that the Win32 API function Sleep() has already been added into this section. The reason we introduced this Win32 API function is because we need to delay some period of time during the loop testing in our program.


Figure 2-62.


Now, click the Form item from the Object list to open the Form_Load event procedure and add the initialization code shown in Figure 2-63 into this procedure.

  1. We still use the Form's properties, Left and Top, to define the initial position of the Form window as it is displayed on screen.

  2. Then we initialize the initFlg to False to indicate to the system that the program is not initialized and the Init() DLL function is needed to call to finish this initialization. Also, we use the Default property of the command button cmdInit to set this button as a default button, which means that this command button is a default active button and it has been enabled as the program runs.


Figure 2-63.


Now, click the cmdInit item from the Object list to open its event procedure and enter the code shown in Figure 2-64 into this procedure.

  1. First, we create a local variable ret, which is used to receive the returned status of calling the Init() function. Then we call the Init() function to initialize the DLL functions, threads, and events. This initialization will create and run two threads and five events in the Visual C++ domain.

  2. An error message will be displayed on screen if this initialization has failed, and the program will be terminated.

  3. After the initialization is completed, the initFlg is set to True, and this flag is used to indicate to the system that the initialization has been completed for the DLL functions. Also, for the user's convenience, the txtLoop textbox has been focused, and the user can directly enter some number to this textbox without first clicking the mouse to get it focused.


Figure 2-64.


After the initialization, we need to code the test section that will interface to the multiple-task DLL we developed in Visual C++. Click the cmdSend item from the Object list to open its event procedure. Enter the code shown in Figure 2-65 into this event procedure to finish the coding for this event.

  1. A local variable ret is declared at the beginning of this event procedure. The purpose of this local variable is to receive the feedback status of executing the SendData() function.

  2. Before we can call the SendData() DLL function, we need to first check the program status to confirm that the initialization is completed for the DLL environment, which includes creating the threads and events in the Visual C++ domain. An error message will be displayed on screen to remind the user to execute the initialization to the DLL domain if the initFlg is False, which means that no initialization is completed.

  3. If the initialization has been completed, a random number is created by calling the Rnd function in Visual Basic. This random number will work as a data and will be sent to the Visual C++ domain by calling the SendData() DLL function. Also, this random number will be displayed in the txtSend textbox.

  4. If the SendData() function is executed successfully, a 0 will be returned to the Visual Basic domain; otherwise, a non-zero value will be returned. In that case, a message with error information will be displayed on screen, and the program will be terminated.


Figure 2-65.


Next, let's code for the cmdRead event procedure. The purpose of this event procedure is to call a DLL function ReadData() to pick up a data, which is sent out before by Visual Basic from the shared data buffer in the Visual C++ domain. This DLL function will set the vbReadEvent event to inform a WaitForMultipleObjects() system function in Thread II to begin to process to pick up the next data in Visual C++. Click the Object list and select the cmdRead item to open this event procedure. Enter the code shown in Figure 2-66 into this procedure.

  1. A local variable ret is created at the beginning of this event procedure, which is used to receive the feedback status of the executing ReadData() function later on.

  2. Before we call the ReadData() DLL function to read a data from Visual C++, we need to check the program status to see if the initialization has been completed for the DLL functions. An error message will be displayed if the initialization has not been done, and the program will exit the procedure. In that case, the user needs to click the Initialize button to perform the initialization.

  3. After the initialization is completed, the ReadData() DLL function should be called to pick up a new data from the DLL function. This DLL function will set the vbReadEvent event and inform a system function in Visual C++, WaitForMultipleObjects(), which is located in Thread II, that Visual Basic is ready to pick up the next data from the shared data buffer. This DLL function will return a data to Visual Basic.

  4. If the returned data is not identical to the random data that was sent out previously by Visual Basic, then the data translated between two threads in Visual C++ has something wrong and the testing failed. A message box with error information will be displayed on screen, and the program will exit this procedure.

  5. Otherwise, the test is successful. The returned data is displayed in the txtRead textbox and can be compared to the data displayed in the txtSend textbox.


Figure 2-66.


The next procedure is the Loop test procedure. Recall that in the last example, a Timer is used to periodically send out and read back a random data to or from a serial port via a DLL developed in Visual C++. This Timer works just like a controller to coordinate between sending and reading data in the Visual Basic domain, which means the Timer will execute sending out a data and reading back a data alternately by using the CPU. In this example, we don't need the Timer; Visual Basic can talk directly to DLL functions to finish a similar functionality as in the previous example, because the DLL is developed in a multiple-task mode; Visual Basic can even call SendData() and ReadData() DLL functions simultaneously.

The Loop test is for this purpose. In the Code window, select the cmdLoop item from the Object list to open the cmdLoop event procedure, and enter the code shown in Figure 2-67 into this procedure.

  1. Three local variables are declared in this procedure. The n works as a loop variable, and the lpNum works as an upper bound of the number of loops. The ret works as a returned status for the DLL functions.

  2. The program still needs to check the initialization status first. An error message will be displayed if initialization is not completed, and the program will exit the procedure and ask the user to click the Initialize command button to perform the initialization to the DLL functions.

  3. Next, we need to check the loop number entered by the user to confirm that the number of loops is a valid value. A message box with warning information will be displayed if either no input has been entered in the loop textbox, or the number entered is an invalid value. In that case, the program exits the procedure and asks the user to re-enter a correct value for this loop number.

  4. If the entered value is a valid number, we convert and assign this value to the local variable lpNum by using the Val system function. The advantage of using the Val function to convert a property to a variable is that both the executing time and the memory space occupied by the variable can be significantly reduced compared with a property of a component.

  5. The code in this section is a core of the loop testing. A for loop is utilized for the loop test. First, the event procedure cmdSend is called to execute sending a data to the Visual C++ domain. Please note that both user-defined Sub or Function and event procedures can be treated in the same way, which means that you can consider both as equivalent, and you can call an event procedure just as you call a user-defined Sub; it is legal. Of course, you can define a Sub, put the same code as the cmdSend procedure did into that Sub, and call that Sub to execute sending a data, but that is not good coding style.

    After executing the event procedure to send a data to Visual C++, we need a little delay to allow the DLL functions to finish the data translation and coordination between multiple tasks. A Win32 API function, Sleep(), is used here to finish this functionality. Following this API function, a Visual Basic system function, DoEvents, is used to help the Sleep() function to finish this delay.

    Ideally, when the Sleep() function is activated, the Sleep() function should temporarily stop executing the current code and return control to the CPU, and the CPU should take over the control and execute other system tasks. When the elapsed time is over, the CPU returns control to the Sleep() function and the latter continues to execute the following codes in the original procedure. But in the real world, sometimes the Sleep() function does not give up and return control to the CPU; it still holds the controllability. If this happens, the system cannot execute other tasks and is not in real sleep status. Using the DoEvents function is a good solution. DoEvents can push and help the Sleep() function return control to the CPU and put the system in real sleep status. In our application, a DoEvents function is attached after each Sleep() function to realize this sleep functionality.

    After executing the cmdSend event procedure, we need to call the cmdRead event procedure to pick up the data we sent out by calling cmdSend. Similarly, a time delay is executed by calling the Sleep() and DoEvents functions.

    You don't need to call the Sleep() function if you prefer not to delay between sending data and reading data. The purpose of this delay is so that we can clearly display both sending-out data and reading-back data in two textboxes, giving the user a clear picture of what is going on in our project.

    If you delete the DoEvents function from this piece of code and keep only the Sleep() function, you will find that the testing loop doesn't run at all when you run this project. The reason for this has already been explained. You can also reduce the delay time and make the loop faster.


Figure 2-67.


Finally, we need to write code for the Exit command button. Select cmdExit from the Object list to open its event procedure. Enter the code shown in Figure 2-68 into this procedure.

  1. A local variable ret is declared here, which is used to hold the returned status of the calling DLL function sExit().

  2. We still need to check the program status to make sure that the program has been initialized. The reason for that is because in the DLL function Exit(), all event handlers and thread handlers should be closed as this DLL function is called. Errors will be encountered if you close those handlers by calling CloseHandle() without creating those events and threads prior to closing them in the program. You cannot close a handler that has not been created in your program.

    It is necessary for us to first check whether the initialization has been completed before we can call this Exit function to do the cleanup jobs in the Visual C++ domain. If no initialization is performed, a message box with error information will be displayed to ask the user to initialize the system first.

  3. If the initialization is completed, call the sExit() function to exit and clean up the environment in Visual C++. Exit is a reserved keyword in Visual Basic; you should use an Alias clause to call that function indirectly.

  4. If the function is executed successfully, a 0 should be returned; otherwise, an error message will be displayed on screen.

  5. Don't forget to place the End command here, which is used to terminate our project in Visual Basic. Without this command, you cannot end your project.


Figure 2-68.


Now everything is done and we can run our project. Press the F5 key on your keyboard to start the project, and the GUI will be displayed on screen, as shown in Figure 2-69.


Figure 2-69.


You will notice that the Initialize button is the default button, because it is surrounded by a dashed line. If you click any button except Initialize, a message box will pop up to remind you to initialize the system, as shown in Figure 2-70(a).


Figure 2-70(a).



Figure 2-70(b).


Click the Initialize button to initialize the system. Then click the Send button to send a random data to the Visual C++ domain. The random data that is sent out will be displayed in the txtSend textbox. Click the Read button to pick up the data that is sent out; that data will be displayed in the txtRead textbox, as shown in Figure 2-69. By continuously clicking the Send and Read buttons, you can test and compare the sent-out data with the read-back data one by one.

Now we can do the loop test. Click the Loop button; a message box will be displayed on screen to ask you to enter a loop number. If you didn't enter a number in the Loop Number textbox you will get the error message, as shown in Figure 2-70(b). Now, enter a number, say, 30, and then click the Loop button again. This time you will find that the loop test runs and the sent-out data and read-back data are displayed in two textboxes alternately, as shown in Figure 2-71. The interval between displaying the two data is about 200 ms.


Figure 2-71.


You can repeat the test by clicking the Send, Read, or Loop buttons. The result should be satisfactory. Now, click the Exit button if you want to terminate the project. That is all for this project. We have successfully developed a multiple-task DLL and have interfaced it with our Visual Basic project.

All files of this project, including the source files, header file, DEF file, and DLL file developed in Visual C++, and the source file, GUI file, and executable file developed in Visual Basic, are included on the accompanying CD-ROM in the Chapter 2VBMultTask folder. You can run this project directly by double-clicking the executable file VBMult.exe. But you need to copy the DLL file VBMultTask.dll to a searchable directory on your computer before you can run this project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.80.94