VB5DLL.TXT Notes for Developing DLLs for use with Microsoft (R) Visual Basic (R) Version 5.00 (C) Copyright Microsoft Corporation, 1996 ======== Contents ======== 1 New Features 2 Calling Conventions 3 16/32-bit Conversion Issues 4 Passing and Returning Common Built-in Types 5 Passing and Returning Strings 6 Passing and Returning User-Defined Types 7 Passing and Returning Objects 8 Passing and Returning Arrays 9 Manipulating Variants 10 Passing Unicode Strings 11 Creating a Type Library 12 Compiling a 32-bit Unicode DLL 13 Creating a TypeLib For All The Functions In This Document 14 Additional Resources NOTE: The samples were created using VC++ 4.2, mktyplib version 2.03.3023. For older versions of VC++., you might need to include the header file ole2.h along with windows.h. ======================= 1: New Features ======================= * Ability to declare a parameter as an actual UDT type in a typelib. In prior versions, we had to use a declare statement. Refer to section 13 for more details. * Ability to declare void* as a parameter type in a typelib. Refer to section 13 for more details. ======================= 2: Calling Conventions ======================= Calling convention determines how arguments are passed onto the stack when a function is called and whether the caller or the called function cleans up the stack when the function returns. When using functions written in a different languages (C/C++, Fortran, etc...) with Visual Basic, it is important the dll functions are consistent with the Visual Basic convention. Visual Basic expects functions called from a dll to use the stdcall calling convention. For example in C/C++, you would specify that you want to use stdcall by specifying the identifier _stdcall in the function prototype and definition header. In Fortran, stdcall is the default. In Visual Basic 5.0, if you pass an argument ByVal by either specifying ByVal in the Declare statement or when calling the procedure, the actual value is passed on the stack.(the exception to the rule being the passing of strings, that are handled differently as detailed in section 5). However, if you don't specify ByVal, a pointer to the value is passed on the stack. DLLs actually get loaded into memory only when a function in the DLL is called for the first time. =============================== 3: 16/32-bit Conversion Issues =============================== 1) Size of data types. Traditionally, in prior 16 bit versions of Visual Basic, if you needed a byte sized data type, you could use string*1. In Visual Basic 5.0, it is better to use the byte data type since string*1 is a Unicode string that takes 2 bytes. An integer in Visual Basic 5.0 still occupies 2 bytes. In contrast, an integer in a 32 bit DLL written in C/C++ occupies 4 bytes. If you need a 4 byte integer on the Visual Basic side, use a long data type. If you need a 2 byte integer on the C/C++ side use a short. 2) Calling functions from the Win32 API. In Win32, DLL procedure names are case-sensitive; In Win16, they are not. For example, this means that GetSystemMetrics and GETSYSTEMMETICS are distinct function names. However, in this case, only the former is correct as it exists in User32.dll. Now if the normal Declare Statement for this function: Declare Function GetSystemMetrics Lib "User32" (ByVal n As Integer) _ As Integer also exists somewhere else in the project, as say: (Say, the CAPSLOCK button accidentally goes ON when typing the function name) Declare Function GETSYSTEMMETRICS Lib "User32" (ByVal n As Integer) _ As Integer then, this will change the previous definition accordingly as well. This makes no difference on Win16 as function names are not case sensitive, but on Win32, a run-time error will occur because GETSYSTEMMETRICS does not exist in the DLL. To avoid this problem, it is recommended to use the Alias Clause as follows: Declare Function GetSystemMetrics Lib "User32" Alias _ GetSystemMetrics (ByVal n As Integer) As Integer By doing this, you make sure that the name used in the alias is not affected by a conversion (if any); so regardless of what case is used for the name in other parts of the code, the declaration will still refer to the correct procedure name in the DLL. 3) ANSI/UNICODE issues When converting a 16 bit DLL to a 32 bit DLL, perhaps one of the biggest concerns you will run into are the ANSI/UNICODE issues. When writing a DLL, you can consider an ANSI string as a one-byte-per-character string, and a UNICODE string as a two-byte-per-character string. Prior versions of Visual Basic 16 bit use either ANSI or DBCS strings. Visual Basic 5.0 uses UNICODE Strings. Because of this, there are some issues you need to be aware of when writing DLLs for use with Visual Basic 5.0. The 32-bit version of Visual Basic 5.0 maintains UNICODE strings internally. But whenever you pass a string to a DLL, Visual Basic will convert it to ANSI. If you do not want Visual Basic to convert your UNICODE string to ANSI, you should first place the string into a byte array, and then pass the address of the first element of the byte array. Note, however, that this is only true if you are using Declare statements for your DLL. If you create a type library, Visual Basic will use whatever type is indicated by the type library. Please refer to section 11 for more information on type libraries. 4) Exporting functions Normally when compiling 32-bit DLLs using a Microsoft Visual C++ compiler the _declspec keyword is used with the dllexport modifier attribute to enable you to export functions, data, and objects from a DLL. This attribute explicitly defines the DLL's interface to its client, which can be an executable file or another DLL. Declaring functions as dllexport eliminates the need for a module-definition (.DEF) file, at least with respect to the specification of exported functions. dllexport replaces the _export keyword used earlier with 16-bit DLLs. However, when writing 32-bit DLLs that need to be called from Visual Basic 5.0, you need to export your function via the DEF file, by specifying all the function names with an EXPORTS statement. This is because the use of the stdcall calling convention mangles function names, which is the very nature of a C++ compiler, but VB does not understand mangled names. _declspec just puts it in the export table, but does not unmangle the function name. Even if you use a file with an extension of .C for your source code (so that the standard C compiler is used), you need to export functions using a DEF file. The following is an example of a typical DEF file: ; The DEF File LIBRARY vb5dll32 CODE PRELOAD MOVEABLE DISCARDABLE DATA PRELOAD MOVEABLE EXPORTS UpperCaseByRef @1 UpperCaseByVal @2 =============================================== 4: Passing and Returning Common Built-in Types =============================================== The functions in this example demonstrate how to pass variables of common VB built-in types like Byte, Integer, Long, Boolean, Single and Double, both by value and by reference to a DLL, as well as how to return them. Each function, takes a variable of the type by value as its first parameter, and another variable of the same type by reference, as its second parameter. It assigns the byVal parameter to the byRef parameter (which is reflected back in VB), and then modifies the byVal parameter and returns it. #include #define CCONV _stdcall BYTE CCONV PassByte (BYTE byt, LPBYTE pbyt) { *pbyt = byt; return byt + 1; } short CCONV PassInteger (short intgr, short far *pintgr) { *pintgr = intgr; return intgr + 1; } short CCONV PassBoolean (short bln, short far *pbln) { *pbln = ~bln; return bln; } LONG CCONV PassLong (LONG lng, LPLONG plng) { *plng = lng; return lng + 1; } float CCONV PassSingle (float sng, float far *psng) { *psng = sng; return sng + (float)1.99; } double CCONV PassDouble (double dbl, double far *pdbl) { *pdbl = dbl; return dbl + 1.99; } Here is the DEF file. ;vb5dll32 DEF File LIBRARY vb5dll32 CODE PRELOAD MOVEABLE DISCARDABLE DATA PRELOAD MOVEABLE EXPORTS PassByte PassInteger PassBoolean PassLong PassSingle PassDouble The following Visual Basic code calls the above functions: Private Declare Function PassByte Lib "vb5dll32.dll" (ByVal byt _ As Byte, pbyt As Byte) As Byte Private Declare Function PassInteger Lib "vb5dll32.dll" (ByVal _ intgr As Integer, pintgr As Integer) As Integer Private Declare Function PassBoolean Lib "vb5dll32.dll" (ByVal _ bln As Boolean, pbln As Boolean) As Boolean Private Declare Function PassLong Lib "vb5dll32.dll" (ByVal lng _ As Long, plng As Long) As Long Private Declare Function PassSingle Lib "vb5dll32.dll" (ByVal _ sng As Single, psng As Single) As Single Private Declare Function PassDouble Lib "vb5dll32.dll" (ByVal _ dbl As Double, pdbl As Double) As Double Private Sub BuiltIntest() Dim i As Integer, b As Boolean, c As Byte Dim l As Long, s As Single, d As Double Dim ir As Integer, br As Boolean, cr As Byte Dim lr As Long, sr As Single, dr As Double i = 7 b = True c = Asc("R") l = 77 s = 0.7 d = 7.77 i = PassInteger(i, ir) Print i, ir b = PassBoolean(b, br) Print b, br c = PassByte(c, cr) Print Chr$(c), Chr$(cr) l = PassLong(l, lr) Print l, lr s = PassSingle(s, sr) Print s, sr d = PassDouble(d, dr) Print d, dr End Sub ================================= 5: Passing and Returning Strings ================================= Visual Basic maintains variable-length strings internally as BSTRs. BSTRs are defined in the OLE header files as OLECHAR FAR*. An OLECHAR is a UNICODE character in 32-bit OLE and an ANSI character in 16-bit OLE. A BSTR can contain NULL values because a length is also maintained with the BSTR. BSTRs are also NULL terminated so they can be treated as an LPSTR. Currently this length is stored immediately prior to the string. This may change in the future, however, so you should use the OLE APIs to access the string length. A subtle point is if you use a declare statement as opposed to a typelib in VB, the string is NOT coerced into an ANSI string. It is left as a UNICODE string. If you use a declare statement, then the string passed is coerced into an ANSI string. In the below example, please pay special attention to the comments. Please refer to section 10 for more information. You can pass a string from Visual Basic to a DLL in one of two ways. You can pass it "by value" (ByVal) or "by reference". When you pass a string ByVal, Visual Basic passes a pointer to the beginning of the string data (i.e. it passes a BSTR). When a string is passed by reference, Visual Basic passes a pointer to a pointer to the string data (i.e. it passes a BSTR*). Since VB coerces the UNICODE string to ANSI, it allocates temporary memory to store the ANSI string. VB recopies the UNICODE version of the string back to the passed in variable. The following table lists what Visual Basic will pass to a DLL function when passing a string. Version By Value By Reference ------------------------------------ 3.0 LPSTR HLSTR 4.0 BSTR BSTR* 5.0 BSTR BSTR* In Visual Basic 3.0, you could use the Visual Basic API routines to access and modify an HLSTR. In Visual Basic 5.0 you should use the OLE APIs to access a BSTR. The following table lists the Visual Basic 3.0 string-handling APIs, and the OLE equivalents. Visual Basic API OLE API -------------------------------------------------------- VBCreateHlstr SysAllocString/SysAllocStringLen VBCreateTempHlstr SysAllocString/SysAllocStringLen VBDerefHlstr* N/A VBDerefHlstrLen* N/A VBDerefZeroTermHlstr N/A VBDestroyHlstr SysFreeString VBGetHlstrLen SysStringLen VBResizeHlstr SysReAllocStringLen VBSetHlstr SysReAllocString NOTE: The BSTR is a pointer to the string, so you don't need to dereference it. Example ------- The first function in this example takes a Visual Basic string by reference, and returns an uppercase copy of the string. The second function takes a Visual Basic string by value and also returns an uppercase copy of the string. This is similar to the UCase function in Visual Basic. In both cases, the DLL function modifies the passed string, which is reflected back in VB. This happens even when the VB string is passed "ByVal" because what is passed to the DLL function is a BSTR which is a char far*, and thus, it is possible to directly access the memory buffer pointed to by the BSTR. The DEF file is not included. #include #define CCONV _stdcall BSTR CCONV UpperCaseByRef(BSTR *pbstrOriginal) { BSTR bstrUpperCase; int i; int cbOriginalLen; LPSTR strSrcByRef, strDst; cbOriginalLen = SysStringByteLen(*pbstrOriginal); //We are planning to return bstrUpperCase if we are using a declare //statement, we need to use SysAllocStringByteLen instead of SysAllocStringLen //because of the coercion of ansi back to a unicode string on return of the //function. See Note #1 below. bstrUpperCase = SysAllocStringByteLen(NULL, cbOriginalLen); strSrcByRef = (LPSTR)*pbstrOriginal; strDst = (LPSTR)bstrUpperCase; for(i=0; i<=cbOriginalLen; i++) *strDst++ = toupper(*strSrcByRef++); SysReAllocString (pbstrOriginal, (BSTR)"Good Bye"); //On return of this function, bstrUpperCase will be coerced back to //a Unicode string if we are using a declare statement instead of a //typelib. See Note #1 below. return bstrUpperCase; } BSTR CCONV UpperCaseByVal(BSTR bstrOriginal) { BSTR bstrUpperCase; int i; int cbOriginalLen; LPSTR strSrcByVal, strDst; cbOriginalLen = SysStringByteLen(bstrOriginal); //We are planning to return bstrUpperCase if we are using a declare //statement, we need to use SysAllocStringByteLen instead of SysAllocStringLen //because of the coercion of ansi back to a unicode string on return of the //function. See Note #1 below. bstrUpperCase = SysAllocStringByteLen(NULL, cbOriginalLen); strSrcByVal = (LPSTR)bstrOriginal; strDst = (LPSTR)bstrUpperCase; for(i=0; i<=cbOriginalLen; i++) *strDst++ = toupper(*strSrcByVal++); SysReAllocString (&bstrOriginal, (BSTR)"Good Bye"); //On return of this function, bstrUpperCase will be coerced back to //a Unicode string if we are using a declare statement instead of a //typelib. See Note #1 below return bstrUpperCase; } The following Visual Basic code calls the above two UpperCase functions: Private Declare Function UpperCaseByRef Lib "vb5dll32.dll" (Str _ As String) As String Private Declare Function UpperCaseByVal Lib "vb5dll32.dll" _ (ByVal Str As String) As String Private Sub StringTest () Dim Str As String, NewStr As String Str = "Hello World!" MsgBox "In VB, Before: " & Str NewStr = UpperCaseByRef(Str) MsgBox "In VB, After: " & Str MsgBox "In VB, CapsStr: " & NewStr Str = "Hello World!" MsgBox "In VB, Before: " & Str NewStr = UpperCaseByVal(Str) MsgBox "In VB, After: " & Str MsgBox "In VB, CapsStr: " & NewStr End Sub Note #1: As an example, if we pass in the string "abs" and we used SysAllocStringLen, then it would allocate a string of length 6. On return of the function, it will coerce the string into Unicode also changing the prefixed length of the BSTR to 12 which is double the amount, therefore we would get a larger string and potential garbage characters. By using SysAllocStringByteLen, it allocates a string of length 3. On return, it coerces the ANSI string back to Unicode string of the proper length 6. ============================================ 6: Passing and Returning User-Defined Types ============================================ With Visual Basic 5.0, you can pass a user-defined type (UDT) by reference to a function. In addition to this, Visual Basic 5.0 also allows functions to return a user-defined type. You still cannot pass a UDT by value to a DLL function. A user-defined type is returned in the same way it would be with any other function, as demonstrated in code example below. It is important to make sure that the UDT as defined in Visual Basic is the same size as the C structure defined in the DLL by enforcing a strict one-to-one alignment of corresponding members in both UDTs. This can be a problem because VB5 uses dword (4-byte) alignment; while the DLL uses the "Struct Member Alignment" Compiler Option as specified by the program developer. Thus, an odd-sized string in VB may cause this problem. In a 32-bit Application, it is efficient\optimal to use a 4-byte Struct Member Alignment, which is what VB uses. In the 32 bit dll, please make sure that you select the 4-byte struct member alignment compiler setting. If you need to call a DLL function that takes a C struct by reference from VB, you should keep in mind a simple rule while designing its counterpart UDT in VB. The rule is that if the size of the struct member is less than the "Struct Member Alignment" size and if the next member can fit in the same alignment size then they will be packed together. Thus, the order in which the members are declared inside a struct is also important. For instance, in the example below, the "byt" member is packed together with the "bln" member because of the above rule and it is followed by a 1 byte padding because the next member which is a VARIANT structure is 16 bytes, which is greater than the 4 byte struct alignment size being used. However, if the location of the "vnt" member and the "strg" member were swapped, then there would be no padding and the 11 character of "strg" would occupy consequent memory locations immediately after the "byt" member. This is because the size of a char is 1 byte which is less than the alignment size of 4. Please refer to section 14 on other references that talk about structure padding. As already mentioned in section 3, another thing to keep in mind is that an integer in VB is always 2 bytes, whereas an "int" in C is 4 bytes on Win32, but 2 bytes on Win16. So, on Win32, this might pose a problem when using an array of integers in VB. The corresponding array of "int"s in C will take up 2 extra bytes and a huge amount of padding may be required in the VB UDT. Hence a short is used, which is 2 bytes (on both win16 and win32). If the C type is int then the VB type will have to change from an integer to a long when going from 16 to 32 bits. Otherwise, if short and long are used in C, then the corresponding integer and long types can be used in VB, regardless of the platform. It is usually preferable to place all odd-sized fields at the end of the user-defined type. This way even-sized fields will be on at least a WORD (2-byte) boundary, and are thus more efficiently accessed. Example ------- The function in this example, shows how to pass and return a user-defined type (UDT) containing all possible data types that can be members, from a DLL function called from Visual Basic. It takes a UDT by reference and returns a copy of the UDT to Visual Basic. The DLL function modifies the members of the passed UDT which is reflected back in VB. NOTE: The Size of the UDT passed to the DLL will be 76 bytes (after all padding. However, the size of the UDT actually stored by VB is 86 bytes, the extra 12 bytes accounting for the fact that the fixed length string member "strg" is stored as a Unicode string, and so the extra 11 bytes for the string and 1 byte for "Unicode padding". //Remember to set compiler struct member alignment to 4 bytes. #include #define MAXSIZE 11 #define CCONV _stdcall typedef struct { short intgr; //integer long lng; //long float sng; //single double dbl; //double double cur; //currency double dtm; //date short bln; //boolean BYTE byt; //byte VARIANT vnt; //variant BSTR vstrg; //variable length string char strg[MAXSIZE]; //fixed length string short array[1][1][2]; //array of integers (2 Bytes in VB) } UDT; UDT CCONV CopyUDT(UDT *pUdt) { UDT udtRet; int i, cbLen; LPSTR strSrc; // Copy Passed-in UDT into the UDT that has to be returned udtRet.intgr = pUdt->intgr; udtRet.cur = pUdt->cur; udtRet.lng = pUdt->lng; udtRet.sng = pUdt->sng; udtRet.dbl = pUdt->dbl; udtRet.dtm = pUdt->dtm; udtRet.bln = pUdt->bln; udtRet.byt = pUdt->byt; udtRet.array[0][0][0] = pUdt->array[0][0][0]; udtRet.vstrg = SysAllocString(pUdt->vstrg); // must initialize all Variants VariantInit(&udtRet.vnt); VariantCopy(&udtRet.vnt, &pUdt->vnt); strncpy(udtRet.strg, pUdt->strg, MAXSIZE-1); // Modify members of passed-in UDT cbLen = SysStringByteLen(pUdt->vstrg); strSrc = (LPSTR)pUdt->vstrg; for(i=0; iarray[0][0][0]++; VariantChangeType(&pUdt->vnt, &pUdt->vnt, NULL, VT_BSTR); pUdt->intgr++; pUdt->lng++; pUdt->sng += (float)1.99; pUdt->dbl += 1.99; pUdt->dtm++; pUdt->bln = ~pUdt->bln; pUdt->byt = toupper(pUdt->byt); strncpy(pUdt->strg, "Bob", MAXSIZE-1); return udtRet; } The following Visual Basic code calls the above UDT function: NOTE: In the Visual Basic UDT, the first member is an integer which is 2 bytes in size. The corresponding member in the C struct is a short, whose size is also 2 bytes. Hence there is a padding of 2 bytes following it, as the next member is a long which is 4 bytes and cannot fit in the same "alignment slot". The "bln" and "byt" member are packed together and there is a 1 byte padding after it. Finally, the "strg" member in the VB UDT is a fixed string of size 22 bytes as stored in VB and 11 bytes when passed to the DLL. The next member is an array of integers. Each member of this array which is of size 2 bytes is actually stored inside the UDT. So, there is no padding after the fixed length string as stored in VB as it can be packed with the last Unicode character of the fixed length string according to the rule above; but there is a padding of 1 byte in the passed UDT (because only 1 byte can be accommodated in the current "alignment slot" and the next member is an array element of size 2 bytes). Private Const MAXSIZE = 11 Private Type UDT intgr As Integer lng As Long sng As Single dbl As Double cur As Currency dtm As Date bln As Boolean byt As Byte vnt As Variant vstrg As String strg As String * MAXSIZE arr(0, 0, 1) As Integer End Type Private Declare Function CopyUDT Lib "vb5dll32.dll"(Src As UDT) _ As UDT Private Const NotEnough As Currency = 100.0067 Private Sub UDTtest() Dim Src As UDT, Cpy As UDT Src.strg = "Robert" + Chr$(0) Src.intgr = 25 Src.lng = 77777 Src.sng = 77777.0178 Src.dbl = 77777.0178 Src.cur = NotEnough Src.dtm = CDate(#11/16/68#) Src.bln = True Src.byt = Asc("m") Src.vnt = 3.14106783 Src.arr(0, 0, 0) = 6 Src.vstrg = "hello world!" Cpy = CopyUDT(Src) Dim Msg As String Msg = "Integer: " & Cpy.intgr & vbCrLf Msg = Msg & "Currency: $" & Cpy.cur & vbCrLf Msg = Msg & "Long: " & Cpy.lng & vbCrLf Msg = Msg & "Date: " & Cpy.dtm & vbCrLf Msg = Msg & "Boolean: " & Cpy.bln & vbCrLf Msg = Msg & "Single: " & Cpy.sng & vbCrLf Msg = Msg & "Double: " & Cpy.dbl & vbCrLf Msg = Msg & "VarType: " & VarType(Cpy.vnt) & vbCrLf Msg = Msg & "VarString: " & Cpy.vstrg & vbCrLf Msg = Msg & "Array: " & Cpy.arr(0, 0, 0) & vbCrLf Msg = Msg & "Name(.): " & Chr$(Cpy.byt) Msg = Msg & ". " & Cpy.strg & vbCrLf MsgBox Msg, vbInformation, "UDT Returned From DLL" Msg = "Integer: " & Src.intgr & vbCrLf Msg = Msg & "Currency: $" & Src.cur & vbCrLf Msg = Msg & "Long: " & Src.lng & vbCrLf Msg = Msg & "Date: " & Src.dtm & vbCrLf Msg = Msg & "Boolean: " & Src.bln & vbCrLf Msg = Msg & "Single: " & Src.sng & vbCrLf Msg = Msg & "Double: " & Src.dbl & vbCrLf Msg = Msg & "VarType: " & VarType(Src.vnt) & vbCrLf Msg = Msg & "VarString: " & Src.vstrg & vbCrLf Msg = Msg & "Array: " & Src.arr(0, 0, 0) & vbCrLf Msg = Msg & "Name(.): " & Chr$(Src.byt) Msg = Msg & ". " & Src.strg & vbCrLf MsgBox Msg, vbInformation, "Modified UDT Passed-In To DLL" End Sub ================================= 7: Passing and Returning Objects ================================= You can pass and return objects from a function in Visual Basic. What Visual Basic is actually passing is a pointer to an OLE interface. All OLE interfaces support QueryInterface, so you can use this interface to get to other interfaces you might need. To return an object to Visual Basic, you simply return an interface pointer. For example, the following ClearObject function will try to execute the Clear method for the object, if it has one. The function will then simply return a pointer to the interface passed in. #include #define CCONV _stdcall LPUNKNOWN CCONV ClearObject(LPUNKNOWN *lpUnk) { LPDISPATCH pdisp; if((*lpUnk)->QueryInterface(IID_IDispatch, (LPVOID *)&pdisp) == NOERROR) { DISPID dispid; DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0}; BSTR name = L"clear"; if(pdisp->GetIDsOfNames(IID_NULL, &name, 1, NULL, &dispid) == S_OK) { pdisp->Invoke(dispid, IID_NULL, NULL, DISPATCH_METHOD, &dispparamsNoArgs, NULL, NULL, NULL); } pdisp->Release(); } return *lpUnk; } The following Visual Basic code calls the ClearObject function: Private Declare Function ClearObject Lib "vb5dll32.dll" (X As _ Object) As Object Private Sub Form_Load() List1.AddItem "item #1" List1.AddItem "item #2" List1.AddItem "item #3" List1.AddItem "item #4" List1.AddItem "item #5" End Sub Private Sub ObjectTest() Dim X As Object ' Assume there is a ListBox with some displayed items on the form Set X = ClearObject(List1) X.AddItem "This should be added to the ListBox" End Sub ================================ 8: Passing and Returning Arrays ================================ When an array is passed to a DLL function, Visual Basic actually passes a pointer to a pointer to a SAFEARRAY structure or an LPSAFEARRAY FAR*. SafeArrays are structures that are used in OLE 2.0. Visual Basic 5.0 uses SafeArrays internally to store arrays. SafeArrays contain information about the number of dimensions and their bounds. The data referred by an array descriptor is stored in column-major order (i.e. the leftmost dimension changes first), which is the same scheme used by Visual Basic, but different than that used by PASCAL or C. The subscripts for SafeArrays are zero-based. The OLE 2.0 APIs can be used to access and manipulate the array. For those familiar with the methods VB 3.0 used, the following table lists the Visual Basic APIs used to reference arrays and the OLE 2.0 equivalents. Visual Basic API OLE API ------------------------------------------------------------- VBArrayBounds SafeArrayGetLBound/SafeArrayGetUBound VBArrayElement SafeArrayGetElement VBArrayElemSize SafeArrayGetElemsize VBArrayFirstElem N/A VBArrayIndexCount SafeArrayGetDim The OLE API function SafeArrayGetDim returns the number of dimensions in the array, and the functions SafeArrayGetLBound and SafeArrayGetUBound return the lower and upper bounds for a given array dimension. All of these functions require a parameter of type LPSAFEARRAY, in order to describe the target array. The first function in this example shows how to get the dimension and bound information of a Visual Basic array (of strings) that is passed to a DLL. It also creates a new array and copies an element of the passed in array into the corresponding index location in the new array. It then modifies the string element at this same index location in the original passed in array (which is reflected back in VB). Finally it stores the new array in a Variant and returns the same to VB. The second function is a shorter version of the first one. It is slightly different in that all the elements from the passed-in array are copied into the new array automatically. In both cases, however, for string arrays, you should first convert the string to Unicode. This is required because VB5 uses Unicode to store strings internally, however it converts them to ANSI on the way in to a DLL. It will normally convert it back to Unicode on the way out of the DLL; but since the string is being copied into an array that has been *NEWLY CREATED INSIDE THE DLL*, VB will not know enough to do the conversion. This example demonstrates passing and returning arrays of strings. But it can easily be modified to work for arrays of any permitted datatype. The only modifications that have to be made are changing the Declare statements and the VT_XXXX flags to match the appropriate type. Of course, you don't have to worry about Unicode conversions when dealing with non-string data-types. Example ------- #include #include #define CCONV _stdcall // hold the SAFEARRAY pointer to be returned in a Variant LPSAFEARRAY lpsa1; LPSAFEARRAY lpsa2; VARIANT CCONV ProcessArray(LPSAFEARRAY FAR *ppsa) { VARIANT vnt; unsigned int i, cdims; char buff[40]; long rgIndices[] = {0,1,2}; BSTR element = NULL; cdims = SafeArrayGetDim(*ppsa); // Must initialize variant first VariantInit(&vnt); // Create an array descriptor if (SafeArrayAllocDescriptor (cdims, &lpsa1) != S_OK) { MessageBox (NULL, "Can't create array descriptor. \n Will return an empty variant", "Error!", MB_OK); lpsa1 = NULL; } // Specify the size and type of array elements if (lpsa1) { lpsa1->cbElements = sizeof(BSTR); lpsa1->fFeatures = FADF_BSTR; } // Get the bound info for passed in array, display it and // store the same in the array to be returned in a variant for (i=1; i <= cdims; i++) { long Lbound, Ubound; SafeArrayGetLBound (*ppsa, i, &Lbound); SafeArrayGetUBound (*ppsa, i, &Ubound); if (lpsa1) { lpsa1->rgsabound[cdims-i].cElements= Ubound-Lbound+1; lpsa1->rgsabound[cdims-i].lLbound = Lbound; } sprintf (buff, "Index %d: Lbound = %li, Ubound = %li\n", i, Lbound, Ubound); MessageBox (NULL, buff, "SafeArrayInfo from DLL", MB_OK); } if (!lpsa1) return vnt; // Allocate space for the actual array elements if (SafeArrayAllocData (lpsa1) != S_OK) { MessageBox (NULL, "can't create array elements","Error!", MB_OK); return vnt; } // Get the value of the string element at (0,1,2). This will // be an ANSI string. SafeArrayGetElement (*ppsa, rgIndices, &element); // Convert this to Unicode, as VB5 will not do // so for you, as the string is inside an array *NEWLY // CREATED* inside the DLL! unsigned int length = SysStringByteLen(element); BSTR wcElement = NULL; wcElement = SysAllocStringLen(NULL, length*2); MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,(LPCSTR)element , -1, (LPWSTR)wcElement, length*2); // Put this Unicode string into the corresponding // location in the array to be returned in a variant lpsa1->fFeatures ^= FADF_BSTR; SafeArrayPutElement (lpsa1, rgIndices, &wcElement); lpsa1->fFeatures |= FADF_BSTR; SysFreeString (element); element = SysAllocString((BSTR)"Good Bye"); // Modify the same element (0,1,2) of the passed-in array SafeArrayPutElement (*ppsa, rgIndices, element); SysFreeString (element); // store the array to be returned in a variant vnt.vt = VT_ARRAY|VT_BYREF|VT_BSTR; vnt.pparray = &lpsa1; return vnt; } VARIANT CCONV CopyArray(LPSAFEARRAY FAR *ppsa) { VARIANT vnt; BSTR element = NULL; long rgIndices[] = {0,1,2}; // Must initialize variant first VariantInit(&vnt); // copy the passed-in array to the array to be returned in // variant SafeArrayCopy (*ppsa, &lpsa2); // Get the value of the string element at (0,1,2). This will // be an ANSI string. SafeArrayGetElement (lpsa2, rgIndices, &element); // Convert this to Unicode, as VB5 will not do // so for you, as the string is inside an array *NEWLY // CREATED* inside the DLL! unsigned int length = SysStringByteLen(element); BSTR wcElement = NULL; wcElement = SysAllocStringLen(NULL, length*2); MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,(LPCSTR)element , -1, (LPWSTR)wcElement, length*2); // Put this Unicode string back into the corresponding // location in the array to be returned SafeArrayPutElement (lpsa2, rgIndices, wcElement); SysFreeString (wcElement); SysFreeString (element); element = SysAllocString((BSTR)"Hello Again!"); // Modify the same element (0,1,2) of the passed-in array SafeArrayPutElement (*ppsa, rgIndices, element); SysFreeString (element); // store the array to be returned in a variant vnt.vt = VT_ARRAY|VT_BYREF|VT_BSTR; vnt.pparray = &lpsa2; return vnt; } The following Visual Basic code calls the above two array functions: Private Declare Function ProcessArray Lib "vb5dll32.dll"(a() As _ String) As Variant Private Declare Function CopyArray Lib "vb5dll32.dll" (a() As _ String) As Variant Private Sub ArrayTest() Dim a(4, 5, 6) As String Dim v1 As Variant Dim v2 As Variant a(0, 1, 2) = "Hello!" Print VarType(v1) v1 = ProcessArray(a()) Print VarType(v1) MsgBox v1(0, 1, 2), vbInformation, "Element Value of Array _ Returned In Variant - 1" MsgBox a(0, 1, 2), vbInformation, "Modified Element Value of _ Passed-in Array - 1" v2 = CopyArray(a()) Print VarType(v2) MsgBox v2(0, 1, 2), vbInformation, "Element Value of Array _ Returned In Variant - 2" MsgBox a(0, 1, 2), vbInformation, "Modified Element Value of _ Passed-in Array - 2 " End Sub ========================= 9: Manipulating Variants ========================= Visual Basic 2.0 introduced a new data type known as a Variant. Developers were able to use the Visual Basic APIs in a DLL to access and manipulate these Variant types. Visual Basic 5.0 also has a Variant data type, but it uses the OLE 2.0 Variant data type. Therefore you must use the OLE 2.0 API functions to access and manipulate Variants. The following table lists the Visual Basic 3.0 APIs used to access Variants, and the OLE 2.0 API equivalents. Visual Basic API OLE API -------------------------------------------------------- VBCoerceVariant VariantChangeType VBGetVariantType N/A* VBGetVariantValue N/A* VBSetVariantValue N/A* * You can get and set the Variant's type and value directly by accessing the Variant's fields. A Variant is really just a structure. Its definition, however, is somewhat complicated. The Variant contains 5 fields. Here is a visual layout of the Variant: +-----+-----+-----+-----+-----------------------+ | A | B | C | D | E | +-----+-----+-----+-----+-----------------------+ Field A contains the Variant type. Fields B, C, and D are reserved for future use, and field E is an 8-byte union that contains any intrinsic type from an Integer to a double-precision floating point number. It can also contain BSTRs, interface pointers, or pointers to one of the intrinsic types. Example ------- The first function in this example demonstrates how to pass a variant by reference to a DLL, makes a copy of it, changes the type of the copy to a string, and then returns the copy. The second function does the same thing, but passes the variant by value. #include #define CCONV _stdcall VARIANT CCONV VariantByRef(VARIANT *pvar) { VARIANT var; // must initialize all Variants VariantInit(&var); VariantCopy(&var, pvar); VariantChangeType(&var, &var, NULL, VT_BSTR); return var; } VARIANT CCONV VariantByVal(VARIANT var) { VARIANT vnt; // must initialize all Variants VariantInit(&vnt); VariantCopy(&vnt, &var); VariantChangeType(&vnt, &vnt, NULL, VT_BSTR); return vnt; } The following Visual Basic code calls the above two variant functions: Private Declare Function VariantByRef Lib "vb5dll32.dll"(var As _ Variant) As Variant Private Declare Function VariantByVal Lib "vb5dll32.dll" (ByVal _ var As Variant) As Variant Private Sub VariantTest() Dim v1 As Variant, v2 As Variant v1 = 3.14159 v2 = VariantByRef(v1) MsgBox VarType(v2) ' v2 should now be "3.14159" v2 = Empty ' Make v2 empty again v2 = VariantByVal(v1) MsgBox VarType(v2) ' v2 should again be "3.14159" End Sub ============================ 10: Passing Unicode Strings ============================ UNICODE is a 16-bit character set capable of encoding all known characters and used as a world-wide character encoding standard. A Unicode string is a two-byte-per character string while an ANSI string is a one-byte-per character string. Win32 API functions that have string parameters are generally implemented in one of three formats: - A generic version that can be compiled for either ANSI or UNICODE - An ANSI version - A UNICODE version The generic function prototype consists of the standard function name implemented as a macro, that maps to the appropriate specific version of the function. The letters A (for ANSI version) and W (for wide-character or UNICODE version) are added to the end of the function names in the specific function prototypes. Windows 95 uses ANSI internally. Windows NT uses Unicode exclusively at the system level. Visual Basic 5.0 maintains Unicode strings internally. Whenever Visual Basic passes a string to a DLL function, it always converts it to ANSI on the way out and back to UNICODE on the way in. You can always call the ANSI version of the APIs from VB on either platform. The only way for VB to call the UNICODE version of the APIs from either platform is to create a type library resource for the DLL, register it in the system registry and then add a reference to it from VB. This will allow you to call the UNICODE version of the API functions without an explicit Declare Statement because Visual Basic will then use whatever type is indicated by the type library. Note that Windows 95 currently does not support the UNICODE version of the APIs. It just has "placeholder" entry points for the UNICODE (W) API functions. Visual Basic 5.0 will coerce every string passed to a DLL to ANSI on the way out of Visual Basic and back to UNICODE on the way in. In order to force Visual Basic 5.0 to always pass Unicode strings, you can create a type library for the DLL and then add a reference to the typelib from Visual Basic 5.0. This is one good reason to use type libraries instead of declare statements. We can now work with VB's internal data types avoiding the coercion of types. Unfortunately, there is a weakness of a type library. We won't be able to work with fixed length strings as we do with variable length strings. Right now, one workaround is to work with a byte array. Refer to section 13 for more information. Type libraries are built using a language known as the Object Description Language (ODL). An ODL file is similar to a C file, but contains additional OLE 2.0 specific additions. The ODL file needs to be compiled to a type library (.TLB) file using the MKTYPLIB utility that comes with Microsoft Visual C++. ============================ 11: Creating a Type Library ============================ With Visual Basic, you can make calls into a DLL by declaring the DLL function inside your Visual Basic code. However, it is also possible to make a type library resource for your DLL. The advantage of this technique is that if users register your type library, then they can add a reference to your DLL from within Visual Basic, and call your DLL routines without an explicit Declare statement. The following code is a sample ODL file you can use to build a 32-bit type library to replace the following Declare statement. Note that the line numbers aren't really part of the ODL file, and are included for reference purposes only. Declare Function square Lib "oletest.dll" (ByVal x As Double) As _ Double 1| [uuid(73ED10A0-BDC5-11CD-9489-08002B3711DB)] 2| library MyLibrary { 3| [dllname("oletest.dll")] 4| module MyModule { 5| [entry("square")] double stdcall square([in] double x); 6| }; 7| }; Line 1: Defines a universally unique identifier (uuid) that will uniquely define this library on any system. (You need to use the GUIDGEN utility that ships with the OLE SDK to create this number.) Line 2: Allows you to specify a name for your library. Line 3: Specifies the name of the DLL that contains the functions in question. The recommended practice,here is *not* to hard-code a path to the location of the dll. When a function is called from this DLL, it will be loaded by searching for it in the standard directories that the LoadLibrary() API uses. NOTE: While debugging, you can enter a hard-coded name but each backslash (\) should be prefixed with another backslash: [dllname("c:\\projects\\oletest.dll")] Line 4: Allows you to specify the name for the Module. (This will be the name that shows up in the Object Browser in Visual Basic) Line 5: You will want to add a line similar to this for each of the functions you are exporting from the DLL. NOTE#1: You could also specify an ordinal entry point. (e.g.: [entry(1)] ...). This gives better performance than named entry points. However, ordinals should not be used for Win API calls because the ordinals are different on different Operating Systems/OS versions. For complete instructions on creating an ODL file and using the MKTYPLIB utility, see Chapter 7, "Object Description Language," of the OLE 2 Programmer's Guide, Volume 2. The following example consists of two parts. The first part is to create a ODL file for KERNEL32.DLL in Visual C++ and compile it into a type library. The second part is to register and reference this type library from Visual Basic and then call the Unicode version of the function in the DLL. The API function used for this purpose is GetPrivateProfileStringW (Note the W denoting the UNICODE version). Note that will work only on Windows NT and not on Windows 95, because Windows 95 does not support the UNICODE version of this API. Creating The ODL File and compiling the TypeLib ----------------------------------------------- 1. Start Visual C++, Version 2.0 or higher. 2. Choose New from the File menu, select Code\Text from the "New" dialog box and click the OK button. A code window titled "Text1" will be created. 3. Add the following code to this code window: [ uuid(13C9AF40-856A-101B-B9C2-04021C007002), helpstring("WIDE Windows API Type Library") ] library WideWin32API { [ helpstring("KERNEL API Calls"), dllname("KERNEL32") ] module KernelAPI { [ helpstring("Gets the value of a .ini file setting."), entry("GetPrivateProfileStringW") ] long _stdcall GetPrivateProfileStringW ( [in] BSTR lpApplicationName, [in] BSTR lpKeyName, [in] BSTR lpDefault, [in] BSTR lpReturnedString, [in] long nSize, [in] BSTR lpFileName ); }; }; 4. Choose Save from the File menu and save this file as "WideApi.odl" 5. From the DOS prompt (or the File Manager) run the following command: MKTYPLIB /I C:\MSVC20\INCLUDE /win32 /tlb WIDEAPI.TLB WIDEAPI.ODL Make sure that the MKTYPLIB.EXE utility that you are using is the 32-bit version. You will find it in the C:\MSVC20\BIN\ directory. If WIDEAPI.ODL is not in the current directory, make sure that you specify the complete pathname in the above command. 6. The type library WIDEAPI.TLB will be created in the current directory. Referencing The TypeLib From Visual Basic ----------------------------------------- 1. Start a new project in Visual Basic. Form1 is created by default. 2. Choose References from the Project menu and register the type library WIDEAPI.TLB by browsing for the file and then clicking the OK button. You should see "WIDE Windows API Type Library" selected in the Available References list. Click the OK button. 3. Add the following code to the Form_Click event of Form1: Dim i As Long Dim sRet As String Dim sSection As String Dim sEntry As String Dim sDefault As String Dim sFileName As String sSection = "Visual Basic" sEntry = "vbpath" sDefault = "hello" sRet = Space(30) sFileName = "c:\windows\vb.ini" i = GetPrivateProfileStringW (sSection, sEntry, sDefault, sRet, _ 30, sFileName) MsgBox i MsgBox sRet Press F5 to run the program. Click on Form1. A message box showing the number of characters read from the vb.ini file will be displayed. Subsequently, another message box will display the value of the requested key from the vb.ini file. ================================== 12: Compiling a 32-bit Unicode DLL ================================== The above examples were written only from the stand point of creating ANSI versions of a DLL. The following example demonstrates how you can modify the above code so that you have a common code base for compiling either the UNICODE or ANSI version of a 32-bit DLL. Note that only 4 functions (UpperCaseByRef, UpperCaseByVal, ProcessArray and CopyArray) need to be changed. The rest are also included anyway, unchanged from their ANSI version. The code for calling these functions from VB is also the same as before. Only you will have to comment out all the DECLARE statements. // comment out the following 2 lines if *NOT* using a TYPELIB, but if // using DECLARE statements for the DLL functions (i.e if compiling // the ANSI version of a 32-bit DLL) #define UNICODE // Windows header files will use Unicode conventions #define _UNICODE // runtime libraries will use Unicode conventions #include #include #include #include #define MAXSIZE 11 #define CCONV _stdcall typedef struct { short intgr; //integer long lng; //long float sng; //single double dbl; //double double cur; //currency double dtm; //date short bln; //boolean BYTE byt; //byte VARIANT vnt; //variant BSTR vstrg; //variable length string char strg[MAXSIZE]; //fixed length string short array[1][1][2]; //array of integers (2 Bytes in VB) } UDT; // hold the SAFEARRAY pointer to be returned in a Variant LPSAFEARRAY lpsa1; LPSAFEARRAY lpsa2; // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! UDT CCONV CopyUDT(UDT *pUdt) { UDT udtRet; int i, cbLen; LPSTR strSrc; // Copy Passed-in UDT into the UDT that has to be returned udtRet.intgr = pUdt->intgr; udtRet.cur = pUdt->cur; udtRet.lng = pUdt->lng; udtRet.sng = pUdt->sng; udtRet.dbl = pUdt->dbl; udtRet.dtm = pUdt->dtm; udtRet.bln = pUdt->bln; udtRet.byt = pUdt->byt; udtRet.array[0][0][0] = pUdt->array[0][0][0]; udtRet.vstrg = SysAllocString(pUdt->vstrg); // must initialize all Variants VariantInit(&udtRet.vnt); VariantCopy(&udtRet.vnt, &pUdt->vnt); strncpy(udtRet.strg, pUdt->strg, MAXSIZE-1); // Modify members of passed-in UDT cbLen = SysStringByteLen(pUdt->vstrg); strSrc = (LPSTR)pUdt->vstrg; for(i=0; iarray[0][0][0]++; VariantChangeType(&pUdt->vnt, &pUdt->vnt, NULL, VT_BSTR); pUdt->intgr++; pUdt->lng++; pUdt->sng += (float)1.99; pUdt->dbl += 1.99; pUdt->dtm++; pUdt->bln = ~pUdt->bln; pUdt->byt = toupper(pUdt->byt); strncpy(pUdt->strg, "Bob", MAXSIZE-1); return udtRet; } // THIS FUNCTION *HAS BEEN* CHANGED FOR THE UNICODE VERSION! BSTR CCONV UpperCaseByRef(BSTR *pbstrOriginal) { BSTR bstrUpperCase; int i; int cbOriginalLen; LPTSTR strSrcByRef, strDst; #if defined(UNICODE) cbOriginalLen = SysStringLen(*pbstrOriginal); #else cbOriginalLen = SysStringByteLen(*pbstrOriginal); #endif bstrUpperCase = SysAllocStringLen(NULL, cbOriginalLen); strSrcByRef = (LPTSTR)*pbstrOriginal; strDst = (LPTSTR)bstrUpperCase; for(i=0; i<=cbOriginalLen; i++) *strDst++ = _totupper(*strSrcByRef++); SysReAllocString (pbstrOriginal, (BSTR)(TEXT("Good Bye"))); return bstrUpperCase; } // THIS FUNCTION *HAS BEEN* CHANGED FOR THE UNICODE VERSION! BSTR CCONV UpperCaseByVal(BSTR bstrOriginal) { BSTR bstrUpperCase; int i; int cbOriginalLen; LPTSTR strSrcByVal, strDst; #if defined(UNICODE) cbOriginalLen = SysStringLen(bstrOriginal); #else cbOriginalLen = SysStringByteLen(bstrOriginal); #endif bstrUpperCase = SysAllocStringLen(NULL, cbOriginalLen); strSrcByVal = (LPTSTR)bstrOriginal; strDst = (LPTSTR)bstrUpperCase; for(i=0; i<=cbOriginalLen; i++) *strDst++ = _totupper(*strSrcByVal++); SysReAllocString (&bstrOriginal, (BSTR)(TEXT("Good Bye"))); return bstrUpperCase; } // THIS FUNCTION *HAS BEEN* CHANGED FOR THE UNICODE VERSION! VARIANT CCONV ProcessArray(LPSAFEARRAY *ppsa) { VARIANT vnt; unsigned int i, cdims; TCHAR buff[40]; long rgIndices[] = {0,1,2}; BSTR element = NULL; cdims = SafeArrayGetDim(*ppsa); // Must initialize variant first VariantInit(&vnt); // Create an array descriptor if (SafeArrayAllocDescriptor (cdims, &lpsa1) != S_OK) { //Will probably get a compile error if cut and pasted the code from //file. To avoid the error, make sure ths string literal is on one //line. MessageBox (NULL, TEXT("Can't create array descriptor. \n Will return an empty variant"), TEXT("Error!"), MB_OK); lpsa1 = NULL; } // Specify the size and type of array elements if (lpsa1) { lpsa1->cbElements = sizeof(BSTR); lpsa1->fFeatures = FADF_BSTR; } // Get the bound info for passed in array, display it and store // the same in the array to be returned in a variant for (i=1; i <= cdims; i++) { long Lbound, Ubound; SafeArrayGetLBound (*ppsa, i, &Lbound); SafeArrayGetUBound (*ppsa, i, &Ubound); if (lpsa1) { lpsa1->rgsabound[cdims-i].cElements = Ubound-Lbound+1; lpsa1->rgsabound[cdims-i].lLbound = Lbound; } _stprintf (buff, TEXT("Index %d: Lbound = %li, Ubound = %li\n"), i, Lbound, Ubound); MessageBox (NULL, buff,TEXT("SafeArrayInfo from DLL"),MB_OK); } if (!lpsa1) return vnt; // Allocate space for the actual array elements if (SafeArrayAllocData (lpsa1) != S_OK) { MessageBox (NULL, TEXT("can't create array elements"), TEXT("Error!"), MB_OK); return vnt; } // Get the value of the string element at (0,1,2). this will be // an ANSI string. SafeArrayGetElement (*ppsa, rgIndices, &element); #if !defined(UNICODE) // Convert this to Unicode, as VB5 will not do so // for you, as the string is inside an array *NEWLY CREATED* // inside the DLL! unsigned int length = SysStringByteLen(element); BSTR wcElement = NULL; wcElement = SysAllocStringLen(NULL, length*2); MultiByteToWideChar (CP_ACP, MB_PRECOMPOSED, (LPCSTR)element, -1, (LPWSTR)wcElement, length*2); // Put this Unicode string into the corresponding location in // the array to be returned in a variant lpsa1->fFeatures ^= FADF_BSTR; SafeArrayPutElement (lpsa1, rgIndices, &wcElement); lpsa1->fFeatures |= FADF_BSTR; #else // Put the (ANSI) string back into the corresponding location // in the array to be returned SafeArrayPutElement (lpsa1, rgIndices, element); #endif SysFreeString (element); element = SysAllocString((BSTR)(TEXT("Good Bye"))); // Modify the same element (0,1,2) of the passed-in array SafeArrayPutElement (*ppsa, rgIndices, element); SysFreeString (element); // store the array to be returned in a variant vnt.vt = VT_ARRAY|VT_BYREF|VT_BSTR; vnt.pparray = &lpsa1; return vnt; } // THIS FUNCTION *HAS BEEN* CHANGED FOR THE UNICODE VERSION! VARIANT CCONV CopyArray(LPSAFEARRAY *ppsa) { VARIANT vnt; BSTR element = NULL; long rgIndices[] = {0,1,2}; // Must initialize variant first VariantInit(&vnt); // copy the passed-in array to the array to be returned in // variant SafeArrayCopy (*ppsa, &lpsa2); // Get the value of the string element at (0,1,2). this will be // an ANSI string. SafeArrayGetElement (lpsa2, rgIndices, &element); #if !defined(UNICODE) // Convert this to Unicode, as VB5 will not do so // for you, as the string is inside an array *NEWLY CREATED* // inside the DLL! unsigned int length = SysStringByteLen(element); BSTR wcElement = NULL; wcElement = SysAllocStringLen(NULL, length*2); MultiByteToWideChar (CP_ACP, MB_PRECOMPOSED, (LPCSTR)element, -1, (LPWSTR)wcElement, length*2); // Put this Unicode string back into the corresponding // location in the array to be returned SafeArrayPutElement (lpsa2, rgIndices, wcElement); SysFreeString (wcElement); #else // Put the (ANSI) string back into the corresponding location // in the array to be returned SafeArrayPutElement (lpsa2, rgIndices, element); #endif SysFreeString (element); element = SysAllocString((BSTR)(TEXT("Hello Again!"))); // Modify the same element (0,1,2) of the passed-in array SafeArrayPutElement (*ppsa, rgIndices, element); SysFreeString (element); // store the array to be returned in a variant vnt.vt = VT_ARRAY|VT_BYREF|VT_BSTR; vnt.pparray = &lpsa2; return vnt; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! LPUNKNOWN CCONV ClearObject(LPUNKNOWN *lpUnk) { LPDISPATCH pdisp; if((*lpUnk)->QueryInterface(IID_IDispatch, (LPVOID *)&pdisp) == NOERROR) { DISPID dispid; DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0}; BSTR name = L"clear"; if(pdisp->GetIDsOfNames(IID_NULL, &name, 1, NULL, &dispid) == S_OK) { pdisp->Invoke(dispid, IID_NULL, NULL, DISPATCH_METHOD, &dispparamsNoArgs, NULL, NULL, NULL); } pdisp->Release(); } return *lpUnk; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! VARIANT CCONV VariantByRef(VARIANT *pvar) { VARIANT var; // must initialize all Variants VariantInit(&var); VariantCopy(&var, pvar); VariantChangeType(&var, &var, NULL, VT_BSTR); return var; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! VARIANT CCONV VariantByVal(VARIANT var) { VARIANT vnt; // must initialize all Variants VariantInit(&vnt); VariantCopy(&vnt, &var); VariantChangeType(&vnt, &vnt, NULL, VT_BSTR); return vnt; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! BYTE CCONV PassByte (BYTE byt, LPBYTE pbyt) { *pbyt = byt; return byt + 1; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! short CCONV PassInteger (short intgr, short far *pintgr) { *pintgr = intgr; return intgr + 1; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! short CCONV PassBoolean (short bln, short far *pbln) { *pbln = ~bln; return bln; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! LONG CCONV PassLong (LONG lng, LPLONG plng) { *plng = lng; return lng + 1; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! float CCONV PassSingle (float sng, float far *psng) { *psng = sng; return sng + (float)1.99; } // THIS FUNCTION IS *NOT* CHANGED FOR THE UNICODE VERSION! double CCONV PassDouble (double dbl, double far *pdbl) { *pdbl = dbl; return dbl + 1.99; } ===================================================================== 13: Creating a TypeLib For All The Example Functions In This Document ===================================================================== You can do away with the requirement for DECLARE statements, if you create a TypeLib for each of the functions exported from the DLL. However, it is important to realize that VB5 will not automatically convert strings from Unicode to ANSI and back to Unicode when not using DECLARE statements. For functions involving strings, you must compile it as a 32-bit Unicode DLL. This section is a perfect opportunity to introduce an enhancement to VB when calling DLL functions. Previous versions did not allow UDTs as parameter types to DLL functions when using type libraries instead of declare statements. Visual Basic 5.0 now allows a UDT as a valid parameter type. We can also use void* as a valid parameter type in a typelib. Void* is interpreted "as any" on the Visual Basic side. The CopyUDT function declaration below is an example of how to pass a UDT to a DLL function using type libraries. As noted earlier, one drawback with using type libraries currently is we will not be able to avoid the conversion from Unicode to ANSI if we are passing fixed length strings in a UDT to our DLL function. If the UDT contains a fixed length string, then in the type library, one workaround is to use an array of unsigned chars. An array of unsigned chars is interpreted as a byte array on the VB side. Since there is not a great way to copy a string to a byte array, the API function lstrcpy is used to copy strings to byte arrays. The VB function strconv is used to convert the byte array to a string. The following example demonstrates how to build an ODL file for each of the above functions: #define CCONV __stdcall #define MAXSIZE 11 [ uuid(B9421A20-B985-11ce-825E-00AA0068851C), helpstring("vb5dll Type Library Info"), version(1.0) ] library VB5DLL32 { typedef struct { short intgr; //integer long lng; //long float sng; //single double dbl; //double CURRENCY cur; //currency DATE dtm; //date boolean bln; //boolean unsigned char byt; //byte VARIANT vnt; //variant BSTR vstrg; //variable length string //Right now there is no good way to represent a fixed //length string in ODL. unsigned char strg[MAXSIZE]; //fixed length string short array[1][1][2];//array of integers (2 Bytes in VB) } UDT; [dllname("kernel32.dll")] module Win32API{ [ entry("lstrcpyA") ] long CCONV CopyStringtoByteArray ( [out] unsigned char* dest, [in] LPSTR source ); [dllname("vb5dll32.dll")] module VB5DLLAPI { [ [ entry("CopyUDT") ] UDT CCONV CopyUDT ( [in, out] UDT* pUdt ); entry("ProcessArray") ] VARIANT CCONV ProcessArray ( [in, out] SAFEARRAY(BSTR) *ppsa ); [ entry("CopyArray") ] VARIANT CCONV CopyArray ( [in, out] SAFEARRAY(BSTR) *ppsa ); [ entry("UpperCaseByRef") ] BSTR CCONV UpperCaseByRef ( [in, out] BSTR *pbstrOriginal ); [ entry("UpperCaseByVal") ] BSTR CCONV UpperCaseByVal ( [in] BSTR bstrOriginal ); [ entry("ClearObject") ] IDispatch * CCONV ClearObject ( [in] IDispatch **lpDisp ); [ entry("VariantByRef") ] VARIANT CCONV VariantByRef ( [in] VARIANT *pvar ); [ entry("VariantByVal") ] VARIANT CCONV VariantByVal ( [in] VARIANT var ); [ entry("PassByte") ] unsigned char CCONV PassByte ( [in] unsigned char byt, [out] unsigned char *pbyt ); [ entry("PassInteger") ] short CCONV PassInteger ( [in] short intgr, [out] short *pintgr ); [ entry("PassBoolean") ] boolean CCONV PassBoolean ( [in] boolean bln, [out] boolean *pbln ); [ entry("PassLong") ] long CCONV PassLong ( [in] long lng, [out] long *plng ); [ entry("PassSingle") ] float CCONV PassSingle ( [in] float sng, [out] float *psng ); [ entry("PassDouble") ] double CCONV PassDouble ( [in] double dbl, [out] double *pdbl ); }; }; To test the CopyUDT function, here is the modified VB code. Private Sub UDTtest() Dim Src As UDT, Cpy As UDT CopyStringtoByteArray Src.strg(0), "Robert" Src.intgr = 25 Src.lng = 77777 Src.sng = 77777.0178 Src.dbl = 77777.0178 Src.cur = NotEnough Src.dtm = CDate(#11/16/68#) Src.bln = True Src.byt = Asc("m") Src.vnt = 3.14106783 Src.Array(0, 0, 0) = 6 Src.vstrg = "hello world!" Cpy = CopyUDT(Src) Dim Msg As String Msg = "Integer: " & Cpy.intgr & vbCrLf Msg = Msg & "Currency: $" & Cpy.cur & vbCrLf Msg = Msg & "Long: " & Cpy.lng & vbCrLf Msg = Msg & "Date: " & Cpy.dtm & vbCrLf Msg = Msg & "Boolean: " & Cpy.bln & vbCrLf Msg = Msg & "Single: " & Cpy.sng & vbCrLf Msg = Msg & "Double: " & Cpy.dbl & vbCrLf Msg = Msg & "VarType: " & VarType(Cpy.vnt) & vbCrLf Msg = Msg & "VarString: " & Cpy.vstrg & vbCrLf Msg = Msg & "Array: " & Cpy.Array(0, 0, 0) & vbCrLf Msg = Msg & "Name(.): " & Chr$(Cpy.byt) Msg = Msg & ". " & StrConv(Cpy.strg, vbUnicode) & vbCrLf MsgBox Msg, vbInformation, "UDT Returned From DLL" Msg = "Integer: " & Src.intgr & vbCrLf Msg = Msg & "Currency: $" & Src.cur & vbCrLf Msg = Msg & "Long: " & Src.lng & vbCrLf Msg = Msg & "Date: " & Src.dtm & vbCrLf Msg = Msg & "Boolean: " & Src.bln & vbCrLf Msg = Msg & "Single: " & Src.sng & vbCrLf Msg = Msg & "Double: " & Src.dbl & vbCrLf Msg = Msg & "VarType: " & VarType(Src.vnt) & vbCrLf Msg = Msg & "VarString: " & Src.vstrg & vbCrLf Msg = Msg & "Array: " & Src.Array(0, 0, 0) & vbCrLf Msg = Msg & "Name(.): " & Chr$(Src.byt) Msg = Msg & ". " & StrConv(Src.strg, vbUnicode) & vbCrLf MsgBox Msg, vbInformation, "Modified UDT Passed-In To DLL" End Sub To compile this ODL into a type library (.TLB), run the following from the command line: MKTYPLIB /I C:\MSVC20\INCLUDE /win32 /tlb vb5dll32.tlb vb5dll32.odl Once the .TLB file is created, make a reference to it from VB (select the Tools\References... menu) and you are now ready to call any of the above functions without a DECLARE statement. NOTE#1: It is important to use the GUIDGEN.EXE utility to create your own guids for use in your ODL file. Do not use the same guids from this sample for use with your own applications. ========================= 14: Additional Resources ========================= For additional information on using DLLs and typelibs with VB, consult the following resources: Extending Visual Basic with C++ DLLs, Bruce McKinney. This document can be found on the October 1996 MSDN. It can also be found at the following web site. http://www.microsoft.com/oledev/olecom/cpp4vb.htm Although written for VB 4.0, the document contains great information on writing DLL functions in C++ and typelibs for use in VB. He also talks about structure padding. For additional information about OLE and OLE 2.0 APIs, consult the following resources: The OLE 2.0 Programmer's Reference Vol. 1 & 2, Microsoft Press, 1994 Inside OLE, Kraig Brockschmidt