In the last post I covered some tools which allow you to create standard dlls in VB6. Once I got playing with the tool to modify the link process, I thought I would keep going a bit and see what else I could do.
I really wanted a way to link in compiled code written in C, directly into my VB6 projects. This need doesnt come up that often, but it does come up. Maybe you have a decoder written in C, that you dont want to have to port. Or rather do some small operations that C makes easy, but are a mess in VB6. There could also be performance reasons with say a compression routine or similar.
Anyway, the example I went with, was creating 64 bit numbers and showing them as a string. VB6 has the native 8 byte currency type which can house a 64bit number, but it cant manipulate them.
So first up, how can we include code compiled in C into our VB6 binary. This turns out to actually be quite easy, all you have to do is add the compiled .obj file to the linker command line. It also turns out that if your C code defined the functions as exports, the linker will automatically place them in the export table which works for exe or dll the same.
Now to use the functions from VB6, you just access them normally as if it was a dll using the Windows API Declare syntax. So how do you debug it while running code in the IDE you ask? There isnt much point in doing this trick, if you cant debug the other parts of your code in the IDE. Thats easy too. When you write your C code, you compile it as a standard dll and use the Declares to access the library. This lets you debug everything traditionally.
Then when your ready to compile, you use the exact same C object file that made the dll, and just link it into the exe build process.
The trick is, that you wrap your raw API access routines, in another function which tests to see if the code is running in the IDE or not. If it is, then it uses the external API declares. If its running as a compiled code, then it uses a second set of API declares that reference the compiled executable itself.
Since the functions are exported, it all functions as if it was using an external dll, just that no separate file is required for distribution. A sample is below:
unsigned__int64__stdcallto64(unsignedint hi, unsignedint lo){
#pragma EXPORTunsigned__int64 ret=0;
ret = hi;
ret = ret <<32;
ret += lo;
return ret;
}
'for accessing linked in C code
Private Declare Function to64 Lib "project1.exe" _
(ByVal hi As Long, ByVal lo As Long) As Currency
'for debugging in the IDE
Private Declare Function dll_to64 Lib "c_obj.dll" Alias "to64" _
(ByVal hi As Long, ByVal lo As Long) As Currency
Function doto64(hi As Long, lo As Long) As Currency
If isIde() Then
doto64 = dll_to64(hi, lo)
Else
doto64 = to64(hi, lo)
End If
End Function
Dim a As Currency
a = doto64(&H11223344, &H55667788)
While this trick is most often needed for complex math routines you dont want to port to VB, there are times where you will also need to use some Windows API from your C obj file routines.
Math and stuff runs fine, sub function calls should also run fine. But using the Windows API cause some problems. This is because the obj file will contain details on the import table, which will override the settings the VB6 executable requires to run, leading to a corrupted executable.
Dont worry though, there is a way around it. The way I chose to go, was to access any WinApi i needed through dynamically looked up function pointers. To load the pointers, I included an Init function which took the addresses of LoadLibrary and GetProcAddress as arguments to help me boot strap it.
typedefFARPROC (__stdcall*GetProc)(HMODULE a0,LPCSTR a1);
typedefHMODULE (__stdcall*LoadLib)(LPCSTR a0);
typedefint (__stdcall*SysAllocSBL)(void* str, int sz);
typedefint (__cdecl*Sprnf)(char*, constchar*, ...);
typedefint (__cdecl*Strlen)(char*);
GetProc getproc;
LoadLib loadlib;
Sprnf sprnf;
Strlen strln;
SysAllocSBL sysAlloc;
int__stdcallinit(int lpfnGetProc, int lpfnLoadLib){
#pragma EXPORT int h =0;
int failed =0;
//_asm int 3
getproc = (GetProc)lpfnGetProc;
loadlib = (LoadLib)lpfnLoadLib;
h = loadlib("msvcrt.dll");
sprnf = (Sprnf)getproc(h,"sprintf");
strln = (Strlen)getproc(h,"strlen");
h = loadlib("oleaut32");
sysAlloc = (SysAllocSBL)getproc(h,"SysAllocStringByteLen");
if( (int)sprnf ==0) failed++;
if( (int)strln ==0) failed++;
if( (int)sysAlloc ==0) failed++;
return failed;
}
Linking in the resulting obj file lead to no further problems. Given that you wont be using this technique for general programming tasks, Limiting the API you use in your C Stubs is a very realistic design constraint.
Bottom line, if you are doing something complex, it would be worthy of its own DLL anyway.
There are also other ways to use C Object files in your VB6 code. With a little bit more experimentation, and some web searching, I was able to find examples of how to replace entire Module files with Cpp counterparts
as well as only replacing individual functions found in VB6 modules.
Replacing the entire module is a little bit slicker than the export technique, because you dont have to write all the wrappers and manage two sets of declare statements. When in the IDE, it operates normally through the declare. Once compiled, and the module replaced, all the functions are literally running the same code but compiled in. Note however that the VB6 declare syntax, the run time does some extra processing for you marshaling
data from C strings to the COM BSTR type. I do include a string example, but you might have to play around with it some more to get it the way you want it.
As far as replacing individual functions from a module, I took a cheap shot approach, where I opened up the .obj file in binary mode and overwrote the function name declaration in the one you want to override. The linker then uses the implementation you provide in the extra C Obj file automatically.
For entire module replacement, you just replace the vb6 .obj file with the C one.
To help me with these experiments I have extended Jim Whites vb LinkTool.
Examples of all of these techniques are available in the source archive I have put on github.
One other nice addition I made while I was in there was to add a POSTBUILD command so that you could automatically run a command line every time your executable was built. This is handy for copying a dll to other project directories, running upx etc.
Comments: (1)
On 05.05.20 - 9:40am Dave wrote:
misc note, you can use the undocumented VBCompiler section of the vbp file to pass commands to the linker such as: