No, I get notifications via email, so I can react quickly on questions addresses to me asked in this thread. By the way, it's better to target questions relating to something previously posted on bbs.vbstreets.ru right there on bbs.vbstreets.ru :) Even if it is russian-speaking forum, it is absolutely okay if you write your post in English. Registration is still opened. Feel free to contact me if you have any problems with switching VBStreets UI from Russian to English. Someone have mentioned he had some issues with that.
I don't understand clearly what do you mean by "sample project".
Ten years ago I announced that contrary to the popular opinion that VB6 does not support multithreading at all (since using CreateThread() usually causes new thread to crash the whole process) under the hood MSVBVM is thread-safe and multithreading-aware at every step and it's only a question of how to create and initialize a new thread propeprly — and I discovered a way of proper thread creation and initialization.
On VBStreets we use the term "brick" to call ready-to-use module (or bunch of modules) which we post on VBStreets (in specific forum aka "Brick Factory") and which can be downloaded by people and plugged into some VB-project without any tweaks and modifications. Initially, I was planning to make a brick which brings support of multithreading into VB. However, I changed my mind very: implementing such thing purely in form of VB code would be rather hard and tricky. I decided to write CreateBasicThread in C/C++ in form of a separate DLL rather then writing it in VB as a simple VB module.
So, I announced that I am going to develop this DLL and publish it. This was called "Stable Multithreading Brick", even though it wasn't a "brick" in its initial sense anymore. I haven't planned to make this DLL open-source: not that I wanted to conceal a number of secrets and "know hows" (even though there was some), but to prevent a number of incompatible forks from 3rd party enthusiasts to appear.
At some point I make difficult decision to
stop development of stable multithreading brick aka
vbthrds.dll. The main and the only reason for that is the following fact:
To make things work properly my DLL needed to work with significant number of functions/variables/methods from MSVBVM which are neither exported nor live at constant locations (i.e. their addresses change with each new version of MSVBVM60.DLL). Functions and variables were living at arbitrary addresses (version-dependent), the machine code of that functions was changing slightly from version to version which was making it harder to find necessary functions by doing a so-called "signature search". Sizes of structure and offsets of fields of classes/structs were also changing from version to version. I had to write code that locate each function, each variable, determines version of MSVBVMxx.DLL and guess size of structs and offsets of fields accordingly.
I wasn't satisfied with the idea that my
vbthrds.dll will come with "
to use this library you must use that version of MSVBVM60.DLL and only that exactly version" warning. I was going to support ALL versions (builds, releases) of MSVBVM60.DLL that exist (so I collected all existing versions and examined them all). Furthermore, I was going to support MSVBM50.DLL for those who use VB5.
Some people love to use packers which embed MSVBVMxx.DLL into main EXE, and some people prefer to rename MSVBVMxx.DLL into something obscure and modify import table of the main EXE/DLL accordingly (to hide the fact they are using VB or I don't know what is their goal exactly). Given that facts, you should be prepared for the situation when multiple DIFFERENT versions of MSVBVM co-exist in the address space of our process. Even without these rare cases with packers or renaming of MSVBVM, you can simply get MSVBVM60.DLL and MSVBVM50.DLL both loaded into address space of your process if your main EXE (compiled using VB6) is using, for example, an OCX-control that is written in VB to (but compiled using VB5). Both may invoke CreateBasicThread: they are doing that independently, but from our point of view (vbthrds.dll) they use different runtimes (MSVBVM libraries). So, to be more robust, you can't just find all required internal entities. As an author of vbthrds.dll I had to write a mechanism which keeps track of all runtimes concurrently loaded into address space of our process and for each module (Windows terminology, not "module" in VB terminology) we need to locate all of required internal (not exported) entities and calculate all sizes and offsets.
Even with all that complex stuff, we still had a chance of Microsoft releasing a new version of MSVBVM60.DLL which my "multithreading brick" won't deal with. Because some minor but still significant change can broke my mechanisms responsible for locating variables/functions/fields offsets.
This work together with another initiative (known as "
msvbvm61.dll" — an idea of patching original MSVBVM60.DLL and embedding new functions/classes/controls and adding support of Unicode for all embedded controls/functions) led me to the conclusion that
all such things require referencing LOTS of internal MSVBVM's functions and vars (which are not explorted, again) and sophistiacted mechanisms required to locate all of them are so hard and exhausting to make and support (they need to be prepared to work with any of existing versions of MSVBVM, and there were more than 10 of builds) comparing to how simple it would be if you have original MSVBVM60.DLL sources and you can just link to any of that internal functions or add any field to any structure or just remove any line of code instad of patching/intercepting machine code.
So I decided to focus on a project which I referenced above as "VB-RCE". All the wishes and desires that was there around VB6 for years (full Unicode support, multithreading, new functions and embedded classes (like Collection class)) becomes way easy to make if you doing it at the level of source code rather than by patching/intercepting compiled binaries. Even if reverse engineering of MSVBVM60 seems to be extremely hard task, it opens up huge perspectives. And, yet again, you haven't to reverse/decompile 100% of code before you can do any (even simpliest) modification.
That doesn't cancels the fact that vbthrds.dll was good idea and I had working prototypes. I just stopped its development in the name of switching to more comprehensive approach.
But vbthrds.dll wasn't open-source and I haven't been finished. I can't remenber if I event posted semi-functional (alpha) builds of vbthrds.dll.
And you asking me for a sample C/C++ project. What do you mean? Do you want me to publish the whole source code of incomplete vbthrds.dll? Do you wan't me to post source code of
CreateBasicThread function?
Here is source code of
CreateBasicThread as it is with all its debug/temporary/raw snippets:
Code:
HANDLE WINAPI CreateBasicThread(LPSECURITY_ATTRIBUTES lpThreadAttr,
LPTHREAD_START_ROUTINE lpStartProc,
LPVOID lpAuxParam)
{
HMODULE hProjModule;
SMT_VBPROJECT_DESCRIPTOR* pProjDescriptor = NULL;
SMT_VBRUNTIME_DESCRIPTOR* pRtDescriptor = NULL;
//
// Определяем модуль проекта, которому принадлежит процедура.
//
hProjModule = PeFindContainerOfSymbol(lpStartProc);
if(hProjModule == NULL)
{
// TODO: Set Error code
return NULL;
}
//
// Просим менеджер кеша предоставить нам дескриптор для данного проекта.
// Менеджер кеша либо создаст новый дескриптор, либо вернёт уже существующий.
//
SmtCmProvideCacheEntriesForHmod(hProjModule,
&pProjDescriptor,
&pRtDescriptor);
if(pProjDescriptor == NULL)
{
// TODO: Set error code
return NULL;
}
//
// DBG: Патчим проджект, TODO: fix it
//
SMT_CTHREADPOOL_SFTABLE &pctp = pRtDescriptor->Internals.SFTables.CCThreadPool;
Project* pProj;
CThreadPool* pRby_ThreadPool = pRtDescriptor->Internals.Rby_ThreadPool;
pRby_ThreadPool;
pProj = (pRby_ThreadPool->*pctp.FindCachePepiProj)(pProjDescriptor->pProjInfo);
((DWORD*)pProj)[37] = 0;
SMT_START_PARAMETERS* pStartParams;
DWORD fdwSystemFlags;
HANDLE hNewThread;
DWORD iNewThreadId;
CONTEXT ntctx;
//
// Мы всегда создаём новый поток в замороженном состоянии, чтобы сформировать
// структуру SMT_START_PARAMETERS в его стеке. Это единственный способ передать
// потоку структуру так, чтобы потом не заботиться об её уничтожении.
//
// Мы не можем переложить ответственность за уничтожение этой структуры на новый
// поток, потому что нет гарантии, что его вообще когда-нибудь разморозят.
// Мы не можем сами заботиться об её уничтожении, потому что для этого потребовалось
// бы отслеживать терминацию всех потоков и держать большую таблицу указателей на
// ждущие уничтожения структуры.
//
fdwSystemFlags = CREATE_SUSPENDED; /* TODO: fix it */
hNewThread = CreateThread(lpThreadAttr,
0, /* TODO: Fix it */
SmtIntermThreadProc,
(LPVOID)SMT_SPB_FAULT_MARKER,
fdwSystemFlags,
&iNewThreadId);
if(!hNewThread) return hNewThread;
//
// Получаем контекст нового потока. Затем сдвигаем указатель стека так,
// чтобы там можно было разместить структуру. И заменяем контекст потока
// новым, изменённым.
//
ntctx.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;
if(!GetThreadContext(hNewThread, &ntctx))
{
TerminateThread(hNewThread, 0);
CloseHandle(hNewThread);
return NULL;
}
ntctx.Esp -= sizeof(SMT_START_PARAMETERS);
pStartParams = (SMT_START_PARAMETERS*)ntctx.Esp;
//
// Win32 API — тупейшая обёртка над Native API, которая убивает кучу востребованных
// возможностей Native API. В частности, мы не можем передать создаваемому потоку
// структуру большого размера, мы можем только передать указатель на структуру,
// но саму структуру мы никак не можем «прикрепить» к потоку. Если бы CreateThread
// позволяла создать в новом стеке некий room, с заведомо известным адресом, который
// бы можно было передать в CreateThread в качестве lpParam... А так нам приходится
// искусственно воссоздавать такую функциональность. Методика основана на знании
// недокументированных особенностей функционирования ядра Windows, а именно функции
// BaseInitializeContext. Если она когда-нибудь изменится, то трюк не сработает,
// поэтому нужно проверить, работает ли BaseInitializeContext так же, как ожидается.
//
if(ntctx.Eax == (DWORD)SmtIntermThreadProc &&
ntctx.Ebx == SMT_SPB_FAULT_MARKER)
{
// BaseInitializeContext работает как ожидалось.
ntctx.Ebx = (DWORD)pStartParams;
}
else
{
//
// BaseInitializeContext работает иначе, а значит новому потоку самому придётся
// искать в своём стеке структуру SMT_START_PARAMETERS, потому что её адрес потоку
// передать никак не удалось. Формируем маркер для поиска...
//
SMT_START_PARAMETERS_MARKER* pSpbMarker;
ntctx.Esp -= sizeof(SMT_START_PARAMETERS_MARKER);
pSpbMarker = (SMT_START_PARAMETERS_MARKER*)ntctx.Esp;
*pSpbMarker = SmtStartParametersBlockMarker;
}
ntctx.Esp -= sizeof(DWORD) * 64; // Отступ ради безопасности
if(!SetThreadContext(hNewThread, &ntctx))
{
TerminateThread(hNewThread, 0);
CloseHandle(hNewThread);
return NULL;
}
//
// Теперь структура лежит на стеке созданного потока, и мы можем спокойно её
// заполнять стартовыми параметрами.
//
pStartParams->UserThreadProc = lpStartProc;
pStartParams->UserThreadParam = lpAuxParam;
pStartParams->Project = pProjDescriptor;
pStartParams->Runtime = pRtDescriptor;
// pStartParams->Flags = 0; /* TODO: Fix it */
BOOL bNeedTlsEmulation = TRUE;
if(bNeedTlsEmulation)
{
//DebugBreak();
LoadLibrary("vbthrds.dll");
LoadLibrary("vbthrds.dll");
LoadLibrary("vbthrds.dll");
LoadLibrary("vbthrds.dll");
EhbtlsAttachToThread(GetCurrentThread());
EhbtlsEnableSection(pProjDescriptor->TlsEmuSection);
}
//
// Теперь наконец даём новому потоку работать.
//
ResumeThread(hNewThread);
return hNewThread;
}
Comments are in Russian, sorry. I presume Google Translate will give poor results as it always does with comments within code.
Here is
SmtIntermThreadProc which is a thunk to caller-supplied ThreadProc and is responsible for proper initialization of runtime library for a newly created thread from context of that new thread:
Code:
DWORD WINAPI SmtIntermThreadProc(LPVOID lpParameter)
{
DWORD ret;
SMT_START_PARAMETERS* psp;
CThreadPool* Rby_ThreadPool;
if(lpParameter == (LPVOID)SMT_SPB_FAULT_MARKER)
{
//
// Наш родитель не смог передать нам указатель на структуру со
// стартовыми параметрами из-за изменений в BaseInitializeContext.
// Ищем её в своём стеке вручную.
//
psp = SmtFindSpbOnThreadStack();
}
else
{
psp = (SMT_START_PARAMETERS*)lpParameter;
}
Rby_ThreadPool = psp->Runtime->Internals.Rby_ThreadPool;
SMT_CTHREADPOOL_SFTABLE& ctpm = psp->Runtime->Internals.SFTables.CCThreadPool;
BOOL bNeedTlsEmulation = TRUE;
EHBTLS_FRAME TlsEmuFrame;
if(bNeedTlsEmulation)
{
EhbtlsEnterThreadSafeRegionEx(&TlsEmuFrame, psp->Project->TlsEmuSection);
}
//
// Вызываем RbyThreadPool.InitDllAccess для инициализации контекста
// данного проекта для нового потока.
//
(Rby_ThreadPool->*ctpm.InitDllAccess)(psp->Project->pProjInfo, psp->Project->hModule);
//
// А теперь вызываем пользовательскую ThreadProc.
//
ret = psp->UserThreadProc(psp->UserThreadParam);
CThreadData* pMyData;
CMsoComponent* pMsoComp;
(Rby_ThreadPool->*ctpm.GetThreadData)(&pMyData);
if(pMyData)
{
pMyData->ThreadAction->ThreadType = VbExeThreadWorker;
*(HANDLE*)((PBYTE)pMyData->ThreadAction + 0x18) = CreateEvent(0, 0, 0, 0);
#ifndef SMT_SUPPORT_OLD_RUNTIMES
pMsoComp = pMyData->MsoComponent;
#else
pMsoComponent = *(CMsoComponent**)
((BYTE*)pMyData + psp->Runtime->Internals.offset_MsoCmpFldInThreadData);
#endif
if(pMsoComp)
{
(pMsoComp->*psp->Project->Runtime->Internals.SFTables.CCMsoComponent.PushMsgLoop)(-1);
}
}
//
// Деинициализируем контекст потока.
//
(Rby_ThreadPool->*ctpm.TerminateThread)();
if(bNeedTlsEmulation)
{
EhbtlsLeaveThreadSafeRegion(&TlsEmuFrame);
}
ExitThread(ret);
return ret; // Этого никогда не произойдёт
}
This two subroutines are only small part of the whole vbthrds.dll.
One of the cruicial components of EHBTLS: exception-handling based TLS. Global variables of single-threaded application are allocated on data section of executable image. However, for multithreaded VB program it isn't acceptable: each thread must have its own copy of the set of all global variables. Since project types such as "ActiveX DLL", "ActiveX Control" and "ActiveX EXE" implies that VB-code can by run in context of multiple different threads, VB (from IDE/compiler point of view) offers "Threading model" option under "Project settings", which can be either "Single Threaded" or "Apartament threaded", and when project is configured to use "Apartament threaded", global variables are allocated either in data section (same as when compiling Single Threaded projects) for main thread of multithreaded project, or in per-thread block of memory for any thread other than main thread. To locate proper instance of global variable VB-code calls vbaAptOffset which returns offset from variable's address in data section to its actual location in per-thread memory block. For the main thread this offset is 0, but for other threads it is a non-zero value so that adding this offset to a address of the variable within data section gives an actual address of this global var instance for a particular thread.
But VB6 IDE and compiler does not allow you to set "Apartament threading" model for projects of type "Standard EXE". Thus, trying to spawn multiple threads within Standard EXE project will lead to a situation where all global variables are shared between all threads. This is barely acceptable for Byte/Integer/Long variables, but is absolutely unacceptable for objects and Variants. To overcome this, my library used SEH/VEH to detect reads and writes from/to global variables and redirect it to proper per-thread memory block containing per-thread copies of all global variables of the project.
Without this you need to be very careful and avoid using global variables. But you can't avoid it completely: App, Forms, Printer, Printers, Clipboard, Screen and so on are actually members of an app-global-object (you can find a class named "Global" in Object Browser) which is referenced via unnamed global variable. So need to emulate TLS for "Standard EXE" projects which might use native TLS facilities of VB but which are actually don't use it due to limitations of IDE/compiler.
Yet another part of vbthds.dll is SMS: it has nothing to do with mobile phones, it stands for Sub Main suppression. If your project has "Startup Object" = "Sub Main" (project properties), Sub Main() gets called each time you create a new thread using my DLL, which is usually not what you want. You can't easily determine from the Sub Main() whether it was called because a program startup or it is called as a side-effect of spawning additional thread. So there was special trickery to suppress invocation of "Sub Main" when new threads were created and initialized via my DLL.
Yet again, these two are great examples of how complex the simple problem becomes when you have to solve it on the wrong level. Having access to the source code of VB (the whole product including IDE/compiler and runtime) and unlocking "Theading Model" for "Standard EXE" or just avoiding unwanted invocation of Main() by simple if-block within MSVBVM code would be much simplier than emulating TLS for complete binary compiled without TLS support or suppressing a Sub Main invocation without patching MSVBVM project startup logic.
It's like having a hand or any other organ: when you born you get your hands "for free" and you take it for granted. But if you loose your hand, it costs you enormous amount of effort and money and even the best of the best prosthetic arms which are very expensive can bring you only limited amount of what you lost.