Years ago I remember reading references that you could link in C object files
into your vb6 projects. This allows you to code some portions in C, yet only
distribute one file and not have to include a separate dll. I also remember
hearing about people working to be able to write standard dlls (not activex)
to use from other languages that did not require COM stuff or having to be
registered on the system before use.
If i remember correctly these were features offered by commercial vb6 addins.
Eventually I got curious and wanted to explore this area of VB6.
As i scoured the net, I eventually found an article and tool released by Jim White
on how to create a standard dll in vb6 for use in other languages.
This tool works by renaming the original link.exe and putting a custom one in its
place to control the link process. He also worked out what kind of dllmain was
required to initialize the vb6 runtime and COM. It was a great piece of research
and I am so glad he released it open source! Thank you Jim! (A copy of the article
is also included in the repo)
From here, I wanted to experiment and figure out how to compile in the C object code.
There are several techniques you can use. The first one I used built directly on his
examples. I added the C object modules to the vb compile, and then added each of the
new functions to the executables export table. They were now accessible through VBs
api declare mechanism. When running in the IDE, you would use the IDE version loaded
from the external dll, when running as an EXE you would use the api declare that aimed
at the exe itself.
Private Declare Function to64 Lib "project1.exe" ( _
ByVal hi As Long, ByVal lo As Long) As Currency
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 IDE Then
doto64 = dll_to64(hi, lo)
Else
doto64 = to64(hi, lo)
End If
End Function
This worked, but its bulky. Re reading some of the other documentation on what other
tools did, they seemed to replace an entire module (.bas) with their own c obj file.
Ok..how did they do that?
In VB, everything is a C++ class, even your modules. They are actually global multiuse
classes. If you look at them in a disassembler you see the following:
void __stdcall Module1::test(Module1 *this) ?test@Module1@@AAGXXZ
Function test(x As String)
MsgBox x
End Function
If we want our C object module to successfully replace the vb6 object module
and link into an executable, we need our C functions to have the exact same
function prototypes. This includes the class structure to get the proper
name mangling. This also means that you have to replace the entire module
and all of its function since they form a single class. After a ton of searching
and experimentation I found that the following works:
//this is the name of the module were replacing..
#define MODNAME Module1
//this structure is required to match the vb6 name mangling of AAGXXZ
class MODNAME
{
private:
void __stdcall MODNAME::init();
void __stdcall MODNAME::to64();
void __stdcall MODNAME::sub64();
void __stdcall MODNAME::add64();
void __stdcall MODNAME::hex64();
};
//since our private class functions actually do have arguments,
//the implementation of the private functions has to redirect
//to the correct implementation. The real implementations are
//also exported from the dll so that they can be used when
//in dll form.
#define JMPFUNC(Name) __declspec(naked) void __stdcall MODNAME::Name () { _asm jmp _##Name }
JMPFUNC(init)
JMPFUNC(to64)
JMPFUNC(sub64)
JMPFUNC(add64)
JMPFUNC(hex64)
Ok, so now we have our vb6 functions being replaced at runtime with the C code.
what about when we are running in the IDE and developing our application?
Since we already have to compile in a VB module with the same interface as a place
holder, we just make it functional by utilizing the exact same functions from the
C code, compiled as a dll.
Private Declare Function dll_init Lib "c_module.dll" Alias "init" (ByVal lpfnGetProc As Long, ByVal lpfnLoadLib As Long) As Long
Private Declare Function dll_to64 Lib "c_module.dll" Alias "to64" (ByVal hi As Long, ByVal lo As Long) As Currency
Private Declare Function dll_add64 Lib "c_module.dll" Alias "add64" (ByVal a As Currency, ByVal b As Currency) As Currency
Private Declare Function dll_sub64 Lib "c_module.dll" Alias "sub64" (ByVal a As Currency, ByVal b As Currency) As Currency
Private Declare Function dll_hex64 Lib "c_module.dll" Alias "hex64" (ByVal a As Currency) As String
Function init(ByVal lpfnGetProc As Long, ByVal lpfnLoadLib As Long) As Long
init = dll_init(lpfnGetProc, lpfnLoadLib)
End Function
Function to64(ByVal hi As Long, ByVal lo As Long) As Currency
to64 = dll_to64(hi, lo)
End Function
Function add64(ByVal a As Currency, ByVal b As Currency) As Currency
add64 = dll_add64(a, b)
End Function
Function sub64(ByVal a As Currency, ByVal b As Currency) As Currency
sub64 = dll_sub64(a, b)
End Function
'not sure why but when going through an API declare, our BSTR return is expanded again on return?
'but when run from the internal obj file is functions as expected...work around for now..
Function hex64(ByVal a As Currency) As String
hex64 = dll_hex64(a)
hex64 = StrConv(hex64, vbFromUnicode)
End Function
Now there are some differences between executing a function through an api declare and an internal function.
Remember from the previous section how the api mechanism converts strings. You will see an example of
having to deal with that in the hex64 function.
A real world project that is already using this technique is Vladimir Vissoultchev's
vbsqlite project. His solution makes allot of sense. He converts all strings manually to C style strings, and then passing in the
string address to the api. This is a good route, especially when integrating with a gigantic
preexisting code base that expects C style strings.
'SQLITE_API int SQLITE_STDCALL sqlite3_open_v2(
' const char *filename, /* Database filename (UTF-8) */
' sqlite3 **ppDb, /* OUT: SQLite db handle */
' int flags, /* Flags */
' const char *zVfs /* Name of VFS module to use */
');
Public Function OpenDb(FileName As String) As Boolean
Dim baName() As Byte
If m_hDb <> 0 Then
Call vbsqlite3_close_v2(m_hDb)
m_hDb = 0
End If
flags = SQLITE_OPEN_READWRITE
baName = pvToUtf8(FileName)
pvResult = vbsqlite3_open_v2(VarPtr(baName(0)),VarPtr(m_hDb), flags, 0)
OpenDb = True
End Function
Private Function pvToUtf8(sText As String) As Byte()
Dim baBuffer() As Byte
Dim lSize As Long
ReDim baBuffer(0 To 4 * Len(sText)) As Byte
lSize = WideCharToMultiByte(CP_UTF8, 0, StrPtr(sText), _
Len(sText), baBuffer(0), UBound(baBuffer), 0, 0)
ReDim Preserve baBuffer(0 To lSize) As Byte
pvToUtf8 = baBuffer
End Function
If you are coding from scratch you could also use Variants instead of strings or compile your C code
to accept LPWSTR pointers and pass in the vb6 strptr (if you arent modifying them).
One of the problems I was having when I was experimenting is that if my C code made use of WinApi I was
getting a corrupted executable. I had to resort to loading the api myself manually in an init() method
and call them all through function pointers that I loaded by passing in references to LoadLibrary and
GetProcAddress.
typedef FARPROC (__stdcall *GetProc)(HMODULE a0,LPCSTR a1);
typedef HMODULE (__stdcall *LoadLib)(LPCSTR a0);
typedef BSTR (__stdcall *SysAllocLen)(void* str, int sz);
typedef int (__cdecl *Sprnf)(char *, const char *, ...);
typedef int (__cdecl *Strlen)(char *);
int __stdcall _init(int lpfnGetProc, int lpfnLoadLib){
#pragma EXPORT
HMODULE h = 0;
int failed = 0;
//_asm int 3
getproc = (GetProc)lpfnGetProc;
loadlib = (LoadLib)lpfnLoadLib;
h = loadlib("kernel32.dll");
mb2wc = (Mb2wc)getproc(h,"MultiByteToWideChar");
h = loadlib("msvcrt.dll");
sprnf = (Sprnf)getproc(h,"sprintf");
strln = (Strlen)getproc(h,"strlen");
h = loadlib("oleaut32");
sysAlloc = (SysAllocLen)getproc(h,"SysAllocStringLen");
if( (int)mb2wc == 0) failed++;
if( (int)sprnf == 0) failed++;
if( (int)strln == 0) failed++;
if( (int)sysAlloc == 0) failed++;
return failed;
}
While this works, its bulky and allot of typing. Thankfully Vladimir was able to come up with a
solution to this problem by adding in the following to the linker command line:
KERNEL32.LIB /OPT:NOREF
"These are required as build.bat in lib compiles sqlite sources with /MD option
(link with MSVCRT.LIB) and then linker needs KERNEL32.LIB to find all used winapi
functions. Option /OPT:NOREF is needed for the final VbSqlite.dll to import
VB6 run-time functions from MSVBVM60.DLL, otherwise the linker will skip
VbSqlite.obj as containing only unreferenced symbols and produce invalid executable."
So there we have it a decent ability to link in C object files directly into your
VB6 executables!
Another bit worth mentioning, Jim's original link tool works off of commands
in a custom .vbc file. I extended this for my experiments. Vladimir wrote
his own link replacement that operates automatically behind the scenes. If it locates
cobj files with the same name as an object file in your project, it will automatically
swap them out. This is handy so you dont have to remember to generate a vbc file or
remember the command file syntax everytime.
One last thing i wanted to experiment with, was the ability to only replace a specific
function in a module. Adding an entirely new module file for just a couple functions would
annoy me. Could this be done? The answer again was yes. Since the linker knows of a functions
existence from the obj file, we can wipe any reference of the vb6 implementation from the vb
produced object file, and then just add in our own new object file that contains that one
function. The linker will now pull from the C implementation and compile that into the final
executable. Its kind of a dirty hack but it works! :)
A couple more notes that Vladimir compiled while researching .obj swapping:
1. In your JMPFUNC macro you are forwarding to _##Name. In sqlite codebase
all functions are prefixed by `sqlite3_` and it made sense to keep the same
names in the .bas file. Unfortunately `__asm jmp Name` is not working as Name
resolves to current function name -- the class MODULE method currently compiled.
I tried '__asm jmp ::Name' explicitly specifiying global scope but apparently
VC6 has troubles with scopes in assembly and errors with "illegal number of
operands" for the jmp instruction. That's why I had to resort 'vbsqlite_'
prefix in .bas file, just to be able to distinguish from original codebase
'sqlite3_' prefix in the JMPFUNC stubs.
2. There is a /FORCE linker option that forces it to produce executable even
there are duplicate symbols found in .obj files. This allows to selectively
replace single .bas module function (w/ surrogate .cobj files for instances),
provided that the original .obj files comes *after* the replacement .cobj
in linker params. The hack comes kind of ugly, but patching .obj files is
way beyond any assessment of beauty :-)) I didn't actually try this method
for selective function replacement but speculate that it could fly with
enough persistence.
|