If you need the devil, then go to hell
He does not need to go far, who has the devil behind him
N.V. Gogol “The Night Before Christmas”
I began the note before last with the words “it has already been 10 years …”, and this one could have been started “it has already been 20 years …”. Although there it was only about stack alignment, but here – about the whole organization of the interaction of the program with WinAPI. I remember that here recently in the comments someone was naively surprised: why do you bring an outdated and uninteresting way of programming through WinAPI? How else can a program interact with the Windows environment if not through calls to its standard functions? Through the available add-ons over WinAPI, not everything can be done.
Of course, it would be great to stay within the paradigm of the programming language used all the time and to “do not stick out the photographer’s ears in the photo”, i.e. so that in the source texts the features of interaction with the environment would not appear in any way. For example, most languages have the concept of a file. To open a file, it is not necessary to explicitly describe a standard function from WinAPI CreateFile or open file, since the compiler will translate the open operator built into the language either directly to a call to this function or to a call to the system library, which somewhere inside itself will call the required function. In any case, the programmer is not required to know exactly how this is implemented in Windows.
In the system library of the PL/1-KT language that I use, there are calls to only 28 WinAPI functions, and this completely covers the “normal” features of the language and one could not care about explicit calls. But alas, often this is not enough. And although normal people go through doors, not windows (oh, what a fresh, sparkling joke!), you have to explicitly refer to functions like CreateWindow or CloseWindow. And it already well does not enter into concepts of language in any way.
Thus, for the fullest use of the capabilities of the Windows environment in a serious language, a mechanism for explicitly calling WinAPI is also required.
When I first started translating my programming system “under Windows” instead of MS-DOS, the task of calling WinAPI seemed simple and did not require any additions to the language itself. With such rich possibilities! With separate translation and procedures description statements from other modules. Called and everything. But, alas, no matter how hard I tried, it was not possible to manage with the existing operators and keywords of the language. Well, there were no “Friend” cigarettes in Shakespeare’s time, that is, ugh, there were no such dynamically linked libraries during the development of PL / 1. And all WinAPIs are implemented in this way.
Let me explain what’s the matter. Previously, the linker bundled all object modules and libraries (or only required parts of libraries) into a single executable module. No other information other than the name of the external function or procedure was required by the linker. The library file itself was either “hooked up” by default or its name was explicitly indicated in the list of combined modules.
For the mechanism DLL just the library file itself is not required at the stage of combining object modules into an executable program. A special section of each EXE file called the “import section” stores information about the names of libraries that need to be loaded into memory before the program starts. The same section stores the names of the required external routines from these libraries and links to a special address table. When the Windows loader prepares the program to run, it reads the DLLs specified in this section into memory, finds the addresses of the specified external subroutines and functions by name, and substitutes these addresses into the specified locations of the EXE file being launched. And if, for example, a function from a DLL is called in a program in 100 places, this does not mean that its address will also be substituted in the EXE file in 100 places. In fact, the address will only be substituted in a single place, since the DLL implies indirect addressing. Those. there is only one variable containing the address of the function, and it is this variable that is set before starting the program and then used in all instructions for calling the function.
In this part, PL/1 excels because it has objects like ENTRY VARIABLE, i.e. just indirect calls through variables containing the address of functions. Hence, a call to WinAPI from the point of view of PL / 1 is a call ENTRY VARIABLE and no modifications are required.
However, in general, in PL/1 source texts, it was not possible to distinguish between “ordinary” external functions, information about which disappears after the link editor is run, and DLL functions, information about which must be written to the import section. I had to enter a new keyword IMPORT in the description of procedures and functions. This is bad and does not meet the standard, but what to do. A small consolation is that this new keyword automatically adds the attribute to the procedure VARIABLE and therefore at least it is not required to write it in the source texts.
The first level of struggle with WinAPI was passed, and now it was already possible to write something like this in the PL/1 source code:
dcl CloseHandle entry(fixed(31)) import;
But victory was still far away.
The next task is how to specify the names of libraries in the source texts? I tried several options and didn’t like any of them. I didn’t like it, mainly, because you need to remember where which function is located or drag the whole bunch of header files into the program. Therefore, I decided not to write library names in the source texts at all, but to create a special object module as part of the system library, which will indicate which function belongs to which library.
In order to have fewer alterations, I took advantage of the possibility of the used RASM-KT assembler to be transmitted using the directive PUBLIC not only addresses of subroutines and variables, but also just constants.
The source code of the module in assembler looks like this:
; ==================== KERNEL32@DLL ====================
PUBLIC KERNEL32@DLL EQU 01 SHL 16
PUBLIC ACQUIRESRWLOCKEXCLUSIVE EQU KERNEL32@DLL+1
AcquireSRWLockExclusive EQU NOT ACQUIRESRWLOCKEXCLUSIVE
PUBLIC ACQUIRESRWLOCKSHARED EQU KERNEL32@DLL+2
AcquireSRWLockShared EQU NOT ACQUIRESRWLOCKSHARED
PUBLIC ACTIVATEACTCTX EQU KERNEL32@DLL+3
ActivateActCtx EQU NOT ACTIVATEACTCTX
PUBLIC ACTIVATEACTCTXWORKER EQU KERNEL32@DLL+4
ActivateActCtxWorker EQU NOT ACTIVATEACTCTXWORKER
PUBLIC ADDATOMA EQU KERNEL32@DLL+5
AddAtomA EQU NOT ADDATOMA
PUBLIC ADDATOMW EQU KERNEL32@DLL+6
AddAtomW EQU NOT ADDATOMW
PUBLIC ADDCONSOLEALIASA EQU KERNEL32@DLL+7
AddConsoleAliasA EQU NOT ADDCONSOLEALIASA
PUBLIC ADDCONSOLEALIASW EQU KERNEL32@DLL+8
AddConsoleAliasW EQU NOT ADDCONSOLEALIASW
; ==================== MAPI32@DLL ====================
PUBLIC MAPI32@DLL EQU 16 SHL 16
PUBLIC BMAPIADDRESS EQU MAPI32@DLL+1
BMAPIAddress EQU NOT BMAPIADDRESS
PUBLIC BMAPIDETAILS EQU MAPI32@DLL+2
BMAPIDetails EQU NOT BMAPIDETAILS
PUBLIC BMAPIFINDNEXT EQU MAPI32@DLL+3
BMAPIFindNext EQU NOT BMAPIFINDNEXT
To be honest, it looks somehow incomprehensible, but if you look closely, it turns out that here the library names are simply assigned numerical constants that have 16 lower zero bits. And the names of functions in these libraries are constants with the same high part and the usual serial number in the low part.
Having received such a strange object module as input (I called it, of course, “Windows”), the linker simply creates its own internal table, where a certain number corresponds to each name. Then, processing already ordinary object modules, the link editor looks for the names of external links both in this table and in other modules. If it was a regular function, it will be found in one of the modules, its address will be determined, and this address will be substituted in the right places in the future EXE file (using FIXUPP see previous note). If the name is found in this table, the link editor transfers this name and value to the preparation of the future import section.
Please note that there are two names in the table: one in capital letters, the other in both capital and small letters. This is because PL/1 does not distinguish between uppercase and lowercase letters outside of quoted character constants. And PL/1-KT also does not distinguish between coinciding Russian and Latin. Therefore, in addition to the name according to the WinAPI rules, there is also its “alias” according to the PL / 1 rules. In order not to produce new constants, according to the rules of WinAPI, the value of the “true” name is assigned a negative value of the alias name constant. Along the way, the problem of the length of the name is also solved, which in PL / 1 should not exceed 31 characters. Using an alias, the real long WinAPI name can be shortened or even replaced with Russian. However, the Russian name of the WinAPI call is inconvenient, due to the fact that the documentation operates only with English names.
Also note that at this stage, the presence of the keyword in the descriptions IMPORT is not taken into account at all. It is only needed at compile time to process the arguments correctly. Whether it is WinAPI or not, it simply determines the very fact of finding an alias name in this table. If the next name in this table is not found, then the corresponding indirect call variable will remain with a zero value. The system library of the PL/1 itself, before actually starting the program, will check and fill in all zero values of such variables with the address of the subroutine for issuing an error signal. That is, if the name of the WinAPI call is mistaken and it is not in the editor’s system table, a trappable exception with a certain number will occur at the call point.
Such a system for connecting to the WinAPI call program seems strange, but in practice it turned out to be quite convenient. The fact is that the entire WinAPI system (in the form of Win32) is quite conservative and rarely changes. Over the decades of work, it took me only a dozen and a half standard Windows libraries: Kerlel32, User32, GDI32, Shell32, WinMM, ImageHlp, OLE32, NTDLL, WinInet, AdvAPI32, ComCTL32, GDIPlus, WinHTTP, Wsock32, MAPI32, which contain a total of about 9600 different calls. Most of these calls are almost never used, but they are all automatically recorded in the table.
Well, what if it’s some other, or even a third-party library, such as DirectX, what to do? In this case, we usually use the “good old” WinAPI LoadLibrary for dynamic loading during program execution. Here you can write any name in quotes, optional according to the rules of PL / 1, and the library is explicitly indicated here. Strictly speaking, by calling only this function, one could generally replace the entire mechanism described above, but then the source texts would look very cumbersome and ugly.
Thus, the existing implementation of the PL/1 language required minimal alterations to make it possible to use WinAPI almost freely, and at the same time, the source texts almost do not reflect the work in this environment. It only took one new keyword IMPORTmainly so that the arguments of the called function are processed correctly (i.e. according to the rules ABI Windows), in the original implementation of PL/1-KT, the rules for passing arguments did not follow the Windows ABI.
An aliasing mechanism was also needed to get around length and case restrictions on PL/1 names. However, this mechanism is actually transparent to the programmer, although some “photographer’s ears” still stick out in the form of letters A And W at the end of WinAPI names designed to work with strings in either ANSII or Unicode. Here they have to be explicitly stated.
Using a simple program, a special table was automatically compiled in assembly language, displaying all the relationships between the WinAPI names and the libraries in which they are located, as well as between the true names and their aliases, taking into account the name restrictions in PL / 1. This table made it possible, when writing source texts, not to think about which library contains the required function.
In the case of new standard libraries, it is easy to add an existing table or use a standard subroutine LoadLibrarywhich does not require the table to be modified.
As a result, programming in PL/1 (implementation in the form of PL/1-KT) became possible with the help of explicit WinAPI calls, and the need for mandatory header files, usually very large ones, is eliminated, since only the functions used can be described directly in the program text. and without specifying the source-library.
Here is a typical example: for an ordinary calculation program, it is required to display in the title of the Windows console window the percentage of the calculation performed, which is contained in the program, for example, in the variable I. In the program, I just add one line of description of the corresponding call and enter a string variable S:
dcl SetConsoleTitleA entry(char(*) var) import;
dcl I fixed(31);
dcl S char(*) var;
//---- преобразование числа в текст ----
put string(S) edit('Выполнено ',I,'%^@')(a,f(2),a);
//---- собственно вывод в заголовок консольного окна ----
Note: the ^@ characters define the null code in a quoted character constant.
By the way, here I can write the names as you like in uppercase and lowercase letters, the compiler will still translate all the names into uppercase and Latin ones. But when running the linker, the correct name will be placed in the EXE file instead of the alias. Since in this case only one function is required from all WinAPIs, it is possible to describe only one of them, and not drag any header files. Well, and, of course, do not specify the library KERNEL32.DLL.
In the case of the inverse problem, when I myself write a DLL library on PL / 1, restrictions on function names again arise. But in this case, one more minor refinement of the language helps: after the attribute EXTERNAL in the function description, you can add an “external” name that distinguishes between uppercase and lowercase letters, for example:.
MY_FUNCTION: PROCEDURE EXTERNAL('MyFunction');
Those. inside the program, the name is used in capital letters, but for external programs using this DLL, it will be seen as MyFunction.