We will discuss how to use Win32 in C # and other libraries to manage functions without. net.
C # The user often asks two questions: "Why is there another problem?
WriteCodeTo use Windows built-in functions? Why didn't I complete the task for me in the framework ?" When the Framework Team built their. net section, they evaluated
. NetProgramWorkers can use Win32 to complete the work. The result shows that the Win32 API set is very large. They do not have enough resources for all
Win32 APIs write managed interfaces, test and write documents. Therefore, priority can be given to the most important parts. Many common operations have managed interfaces, but there are still many complete Win32
Some do not have managed interfaces.
Platform call (P/invoke) is the most common method to complete this task. To use P/invoke, you can write
Describes how to call the prototype of a function. This information is then used for calling at runtime. Another method is to use managed extensions to C ++ to wrap functions.
The content will be introduced in a later column.
The best way to understand how to complete this task is through examples. In some examples, I only provide some code. The complete code can be downloaded.
Simple Example
In the first example, we will call the BEEP () API to make a sound. First, I need to write appropriate definitions for beep. Looking at the definition in msdn, I found it has the following prototype:
Bool BEEP (
DWORD dwfreq, // Audio Frequency
DWORD dwduration // audio duration
);
To use C # to compile this prototype, You need to convert the Win32 type to the corresponding C # type. Because DWORD is a 4-byte integer, we can use int
Or uint as the corresponding type of C. Since Int Is a cls compatible type (can be used in all. NET languages), it is more commonly used than uint, and in most cases
The differences between them are not important. The bool type corresponds to the bool type. Now we can use C # To compile the following prototype:
Public static extern bool BEEP (INT frequency, int duration );
This is a standard definition, but we use extern to indicate that the actual code of the function is elsewhere. This prototype tells the runtime how to call a function; now we need to tell it where to find the function.
We need to review the code in msdn. In the reference information, we found that beep () is defined in kernel32.lib. This means that the runtime code is included in kernel32.dll. We add the dllimport attribute in the prototype to notify the runtime of this information:
[Dllimport ("kernel32.dll")]
This is all we have to do. The following is a complete example. The random sound generated by it is very common in sci-fi movies in 1960s.
Using system;
Using system. runtime. interopservices;
Namespace beep
{
Class class1
{
[Dllimport ("kernel32.dll")]
Public static extern bool BEEP (INT frequency, int duration );
Static void main (string [] ARGs)
{
Random random = new random ();
For (INT I = 0; I <10000; I ++)
{
Beep (random. Next (10000), 100 );
}
}
}
}
The sound is enough to stimulate any listener! Because dllimport allows you to call any code in Win32, malicious code may be called. Therefore, you must be a fully trusted user before calling p/invoke.
Enumeration and constants
Beep () can be used to make arbitrary sound, but sometimes we want to make a specific type of sound, so we use messagebeep () instead (). Msdn provides the following prototype:
Bool messagebeep (
Uint utype // sound type
);
This looks simple, but two interesting facts can be found in the annotations.
First, the utype parameter actually accepts a set of predefined constants.
Second, possible parameter values include-1, which means that although it is defined as a uint type, int is more suitable.
It is reasonable to use the enum type for the utype parameter. Msdn lists named constants, but does not provide any prompts for specific values. Because of this, we need to check the actual API.
If you have installed Visual Studio? And C ++, the Platform SDK is located in \ Program Files \ Microsoft Visual Studio. NET \ vc7 \ platformsdk \ include.
To find these constants, I executed a findstr in this directory.
Findstr "mb_iconhand" *. h
It determines that constants are located in winuser. h, and then I use these constants to create my Enum and prototype:
Public Enum beeptype
{
Simplebeep =-1,
Iconasterisk = 0x00000040,
Iconexclamation = 0x00000030,
Iconhand = 0x00000010,
Iconquestion = 0x00000020,
OK = 0x00000000,
}
[Dllimport ("user32.dll")]
Public static extern bool messagebeep (beeptype );
Now I can use the following statement to call it: messagebeep (beeptype. iconquestion );
Processing Structure
Sometimes I need to determine the battery condition of my laptop. Win32 provides power management functions.
Search for msdn and find the getsystempowerstatus () function.
Bool getsystempowerstatus (
Lpsystem_power_status lpsystempowerstatus
);
This function contains a pointer to a structure that we have not processed. To process the structure, we need to use C # to define the structure. We start with the definition of unmanaged systems:
Typedef struct _ system_power_status {
Byte aclinestatus;
Byte batteryflag;
Byte batterylifepercent;
Byte reserved1;
DWORD batterylifetime;
DWORD batteryfulllifetime;
} System_power_status, * lpsystem_power_status;
Then, use the C # type instead of the C type to get the C # version.
Struct systempowerstatus
{
Byte aclinestatus;
Byte batteryflag;
Byte batterylifepercent;
Byte reserved1;
Int batterylifetime;
Int batteryfulllifetime;
}
In this way, you can easily compile the C # prototype:
[Dllimport ("kernel32.dll")]
Public static extern bool getsystempowerstatus (
Ref systempowerstatus );
In this prototype, we use "Ref" to specify that the structure pointer will be passed instead of the structure value. This is a general method for processing the structure passed through pointers.
This function works well, but it is best to define the aclinestatus and batteryflag fields as Enum:
Enum aclinestatus: byte
{
Offline = 0,
Online = 1,
Unknown = 255,
}
Enum batteryflag: byte
{
High = 1,
Low = 2,
Critical = 4,
Charging = 8,
Nosystembattery = 128,
Unknown = 255,
}
Note that because the fields in the structure are some bytes, we use byte as the basic type of The enum.
String
Although there is only one. Net string type, this string type has several unique features in an unmanaged application. You can use character pointers and structures with nested character arrays. Each array needs to be properly encapsulated.
There are two different string representations in Win32:
ANSI
Unicode
Originally, Windows used single-byte characters, which saved storage space, but complicated multi-byte encoding was required when processing many languages. Windows NT? When it appears, it uses
Double-byte unicode encoding. To solve this difference, Win32 API adopts a very clever approach. It defines the tchar type on the Win9x Platform.
It is a single-byte character and a double-byte UNICODE character on the WINNT platform. For each function that accepts strings or structures (including character data), the Win32 API
Defines two versions of the structure, specifying ANSI encoding with the suffix, and specifying wide encoding (UNICODE) with W ). If you compile the C ++ program into a single word
The A variant is obtained. If Unicode is compiled, the W variant is obtained. The Win9x Platform contains the ANSI version, while the WINNT platform contains the W version.
Since P/invoke designers do not want you to worry about your platform, they provide built-in support to automatically use version A or version W. If the called function does not exist, the interoperability layer searches for and uses version A or version W for you.
The example can be used to illustrate the subtlety of String Support.
Simple string
The following is a simple example of a function that accepts string parameters:
Bool getdiskfreespace (
Lpctstr lprootpathname, // root path
Lpdword l1_ctorspercluster, // number of sectors of each cluster
Lpdword lpbytespersector, // The number of bytes per slice
Lpdword lpnumberoffreeclusters, // number of available sectors
Lpdword lptotalnumberofclusters // total number of sectors
);
The root path is defined as lpctstr. This is a platform-independent string pointer.
Because there is no function named getdiskfreespace (), the mail collector will automatically find the "a" or "W" variant and call the corresponding function. We use an attribute to indicate the string type required by the API.
The following is the complete definition of the function, as I began to define:
[Dllimport ("kernel32.dll")]
Static extern bool getdiskfreespace (
[Financialas (unmanagedtype. lptstr)]
String rootpathname,
Ref int sectorspercluster,
Ref int bytespersector,
Ref int numberoffreeclusters,
Ref int totalnumberofclusters );
Unfortunately, this function cannot be executed when I try to run it. The problem is that, no matter which platform we are on, the mail collector tries to find the ANSI version of the API by default.
Lptstr indicates that the Unicode string is used on the Windows NT platform. Therefore, if you try to use the Unicode string to call the ANSI function
Will fail.
There are two ways to solve this problem: a simple method is to delete the externalas attribute. If this is done, the version of the function will always be called.
This is a good method if this version is available on all of your platforms. However, this will reduce the code execution speed, because the mail collector needs to extract the. NET string from
Unicode is converted to multi-byte, then the version of the function is called (the string is converted back to Unicode), and the W version of the function is called.
To avoid this problem, you need to tell the mail collector to find version A on Win9x and version w on NT. To achieve this, set charset to a part of the dllimport attribute:
[Dllimport ("kernel32.dll", charset = charset. Auto)]
In my informal timing test, I found this approach was about 5% faster than the previous method.
For most Win32 APIs, you can set the charset attribute for the string type and use lptstr. However, some functions that do not adopt the/W mechanism must adopt different methods for these functions.
String Buffer
The string type in. NET is unchangeable, which means its value will remain unchanged forever. For a function that copies string values to the string buffer, the string is invalid. This will at least break through
Break down the temporary buffer created by the mail collector when converting strings; severely damage the managed heap, which usually leads to errors. In either case, the correct return value cannot be obtained.
To solve this problem, we need to use another type. The stringbuilder type is designed as a buffer. We will use it to replace the string. The following is an example:
[Dllimport ("kernel32.dll", charset = charset. Auto)]
Public static extern int getaskpathname (
[Financialas (unmanagedtype. lptstr)]
String path,
[Financialas (unmanagedtype. lptstr)]
Stringbuilder export path,
Int bytes pathlength );
Using this function is simple:
Stringbuilder export Path = new stringbuilder (80 );
Int result = getmediapathname (
@ "D: \ test.jpg", using path, using path. capacity );
String S = export path. tostring ();
Note that the capacity of stringbuilder transmits the buffer size.
Structure with nested character array
Some functions accept structures with nested character arrays. For example, the gettimezoneinformation () function accepts pointers to the following structures:
Typedef struct _ time_zone_information {
Long bias;
Wchar standardname [32];
Systemtime standarddate;
Long standardbias;
Wchar daylightname [32];
Systemtime daylightdate;
Long daylightbias;
} Time_zone_information, * ptime_zone_information;
To use it in C #, two structures are required. One is systemtime, which is easy to set:
Struct systemtime
{
Public short wyear;
Public short wmonth;
Public short wdayofweek;
Public short wday;
Public short whour;
Public short wminute;
Public short wsecond;
Public short wmilliseconds;
}
There is nothing special here; the other is timezoneinformation. Its definition must be more complex:
[Structlayout (layoutkind. Sequential, charset = charset. Unicode)]
Struct timezoneinformation
{
Public int bias;
[Financialas (unmanagedtype. byvaltstr, sizeconst = 32)]
Public String standardname;
Systemtime standarddate;
Public int standardbias;
[Financialas (unmanagedtype. byvaltstr, sizeconst = 32)]
Public String daylightname;
Systemtime daylightdate;
Public int daylightbias;
}
This definition has two important details. The first is the financialas attribute:
[Financialas (unmanagedtype. byvaltstr, sizeconst = 32)]
View the byvaltstr document. We found that this attribute is used for the embedded character array; the other is sizeconst, which is used to set the size of the array.
When I wrote this code for the first time, I encountered an execution engine error. This usually means that some interoperability overwrites some memory, indicating that the size of the structure is incorrect. I use
Marshal. sizeof () to obtain the size of the used mail collector. The result is 108 bytes. I further investigated and soon recalled the default character types for Interoperability
It is ANSI or a single byte. In the function definition, the character type is wchar, which is double byte, thus leading to this problem.
I corrected it by adding the structlayout attribute. Structure is arranged in order by default, which means all fields are arranged in the order they are listed. The charset value is set to Unicode to always use the correct character type.
After such processing, the function is normal. You may wonder why charset. Auto is not used in this function. This is because it does not have a and W variants, but always uses Unicode strings, so I use the above encoding method.
Functions with callback
When Win32 functions need to return multiple pieces of data, they are usually implemented through the callback mechanism. The developer passes the function pointer to the function and calls the function of the developer for each item.
In C #, function pointers are not provided. Instead, "delegates" are used to call Win32 functions instead of function pointers.
The enumdesktops () function is an example of this type of function:
Bool enum1_tops (
Hwinsta, // handle of the window instance
Desktopenumproc lpenumfunc, // callback function
Lparam // the value used for the callback function
);
The hwinsta type is replaced by intptr, while lparam is replaced by Int. Worker topenumproc requires more work. The definitions in msdn are as follows:
Bool callback enum1_topproc (
Lptstr lpszdesktop, // desktop name
Lparam // user-defined value
);
We can convert it to the following delegate:
Delegate bool enum1_topproc (
[Financialas (unmanagedtype. lptstr)]
String comment topname,
Int lparam );
After this definition is completed, we can write the following definition for enumdesktops:
[Dllimport ("user32.dll", charset = charset. Auto)]
Static extern bool enum1_tops (
Intptr windowstation,
Enumdesktopproc callback,
Int lparam );
In this way, the function can run normally.
When using delegation in interoperability, there is an important technique: The Mail collector creates a function pointer pointing to the delegate, which is passed to the unmanaged function. However, the mail collector cannot determine what to do with the function pointer for an unmanaged function. Therefore, it assumes that the function pointer is valid only when the function is called.
The result is that if you call a function such as setconsolectrlhandler () and the function pointer is saved for future use, you need to make sure that the delegate is referenced in your code. If this is not done, the function may be executed on the surface, but the delegate will be deleted in future memory reclaim processing and an error will occur.
Other advanced functions
The examples I listed so far are relatively simple, but there are still many more complex Win32 functions. The following is an example:
DWORD setentriesinacl (
Ulong ccountofexplicitentries, // number of items
Pexplicit_access plistofexplicitentries, // buffer zone
PACl oldacl, // original ACL
PACl * newacl // new ACL
);
The processing of the first two parameters is relatively simple: ulong is very simple, and unmanagedtype. lparray can be used to block the buffer.
However, the third and fourth parameters have some problems. The problem lies in the way the ACL is defined. The ACL structure only defines the ACL header, and the rest of the buffer is composed of ACE. Ace can have multiple different types, and the length of these different types of ACE is also different.
If you are willing to allocate space for all the buffers and want to use less secure code, you can use C # for processing. However, the workload is huge, and the program is very difficult to debug. It is much easier to use C ++ to process this API.
Other attributes
The dllimport and structlayout attributes have some very useful options to facilitate the use of P/invoke. All these options are listed below:
Dllimport
Callingconvention
You can use it to tell the mail collector which call conventions are used by the function. You can set it as your function call convention. Generally, if this setting is incorrect, the Code cannot be executed. However, if your letter
If the number is a cdecl function and stdcall (default) is used to call this function, the function can be executed, but the function parameters are not deleted from the stack, which causes the stack to be filled up.
Charset
Controls whether to call Variant A or variant W.
Entrypoint
This attribute is used to set the name searched by the package collector in the DLL. After setting this attribute, You can rename the C # function as any name.
Exactspelling
Set this attribute to true, and the feifer will disable the search feature of a and W.
Preservesig
Com interoperability makes the function with final output parameters look like it returns this value. This attribute is used to disable this feature.
Setlasterror
Make sure to call Win32 API setlasterror () so that you can identify the error.
Structlayout
Layoutkind
The structure is arranged in sequence by default and applies in most cases. To fully control the placement of structure members, you can use layoutkind. Explicit and add the fieldoffset attribute to each structure member. This is usually required when you need to create a Union.
Charset
Controls the default character types of byvaltstr members.
Pack
Set the compression size of the structure. It controls the structure arrangement. If the C structure adopts other compression methods, you may need to set this attribute.
Size
Set the structure size. This attribute is not commonly used. However, you may need to allocate additional space at the end of the structure.
Load from different locations
You cannot specify where you want dllimport to find files at runtime, but you can use one technique to achieve this purpose.
Dllimport calls loadlibrary () to complete its work. If a specific dll has been loaded in the process, loadlibrary () will succeed even if the specified loading path is different.
This means that if you call loadlibrary () directly, you can load the DLL from any location, and then dllimport loadlibrary () will use this DLL.
Because of this behavior, we can call loadlibrary () in advance to direct your call to other DLL. If you are writing a library, you can call getmodulehandle () to prevent this situation, to ensure that the Library is not loaded before the first call of P/invoke.
P/invoke troubleshooting
If your P/invoke call fails, it is usually because some types are incorrectly defined. The following are some common problems:
1. Long! = Long. In C ++, long is a 4-byte integer, but in C #, it is an 8-byte integer.
2. the string type is incorrectly set.