Win32 DLL calls Win32 DLL through PInvoke in C #

xiaoxiao2021-03-20  209

Download this article: Net0307.exe (133KB)

I noticed a trend in my recent programming, which is this trend to lead the topic of this month. Recently, I completed a lot of Win32® Interop in applications based on Microsoft® .NET Framework. I am not to say that my app is full of custom Interope code, but sometimes I will encounter some of the times but reflose, insufficient content in the .NET Framework class library, by calling the Windows® API, Can reduce this trouble quickly.

So I think that there is not enough functional limitations in any Windows in .NET Framework 1.0 or 1.1 library. After all, 32-bit Windows (no matter what version) is a mature operating system that serves our customers for more than ten years. In contrast, .NET Framework is a new thing.

As more and more developers go to the managed code, developers have more frequently studying the underlying operating system to find some key functions. It is very natural - at least the time.

Fortunately, the intertency of the public language runtime (CLR) (called platform calls (p / invoke)) is very complete. In this column, I will focus on how to actually use P / Invoke to call the Windows API function. When the CLR's COM Interop function is used, P / INVOKE is used as a noun; when the function is used, it is used as a verb. I don't plan to introduce Com Interop directly because it has better accessibility than P / Invoke, which is more complicated, which makes COM Interop as a column topic to discuss it is not very concise.

Walking P / Invoke

First, start from the inspection of a simple P / INVOKE sample. Let us see how to call the Win32 MessageBeep function, and its non-hosting declaration is shown in the following code:

BOOL messageBeep

Uint utype // beep type

);

In order to call MessageBeep, you need to add the following code to a class or structure definition in C #:

[DLLIMPORT ("User32.dll")]]]]]

Static Extern Boolean MessageBeep (uint32 beeptype);

Surprisingly, only this code can cause the hosted code to call the unmanaged MessageBeep API. It is not a way to call, but an external method definition. (In addition, it is close to a direct port from C and C #, so it is helpful to introduce some concepts as a starting point.) Possible calls from the hosted code are as follows:

MessageBeep (0);

Please note that the MessageBeep method is now declared as static. This is required by the P / Invoke method because there is no consistent example concept in the Windows API. Next, it is also important to note that the method is marked as extern. This is a prompt compiler that this method is implemented by a function exported from the DLL, so there is no need to provide a method body.

Speaking of the lack of method, do you notice that the MessageBeep declaration does not include a method body? Unlike most algorithms, the hosted method consisting of an intermediate language (IL) instruction is different. The P / Invoke method is only metadata, and the real-time (JIT) compiler is running through it to connect the managed code with the unmanaged DLL function at runtime. An important information required to perform this connection to the non-hosting world is to export the name of the DLL of the non-hosting method. This information is provided by the DLLIMPORT custom attribute before the MessageBeep method declared. In this example, it can be seen that the MessageBeep non-managed API is exported by user32.dll in Windows. Up to now, there is no introduction about calling messagebeep, please review the code that the call is very similar to the following code snippet:

[DLLIMPORT ("User32.dll")]]]]]

Static Extern Boolean MessageBeep (uint32 beeptype);

Finally, these two topics are topics related to data encapsulation and actual methods from managed code to non-hosting functions. Calling the unmanaged MessageBeep function can be executed by any hosted code declared by the Extern MessageBeep declaration within the scope. This call is similar to any other call to static methods. It is common to any other hosted method calls that the need for data encapsulation processing.

One of the rules of C # is that its call syntax can only access the CLR data type, such as System.uint32 and System.Boolean. C # Obviously does not identify C-based data types (for example, UINT and BOOL) used in the Windows API, which are just the type definition of C language type. So when the Windows API function messagebeep is written as follows

Bool MessageBeep (uint utype)

The external method must use the CLR type to define, as seen in the previous code snippet. It is necessary to use the CLR type that is different from the basic API function type but compatible with the P / Invoke is difficult to use. Therefore, after this column, I will use a complete section to introduce the data package processing.

style

P / invoke calls for Windows APIs are simple in C #. However, if the class library refuses to make your application beep, you should try to call Windows to make this work, is it?

Yes it is. But it is related to the choice of the choice, and it is very big! Typically, if the class library provides some way to implement your intent, it is best to use the API instead of calling the unmanaged code, because the CLR type and Win32 are very different in style. I can break the suggestions about this issue into one sentence. When you perform P / Invoke, don't give the application logic directly to any external method or the components therein. If you follow this small rule, you often save a lot of trouble from the long run.

The code in Figure 1 shows the minimum additional code of the MESSAGEBeep external method I discussed. There is no significant change in Figure 1, but only some common improvements are made to the unpackable external method, which makes it easier. From the top, you will notice a full type named Sound, which is dedicated to MessageBeep. If I need to use the Windows API function playSound to add support for playback waveforms, you can reuse the Sound type. However, I will not be angry because of the type of public static method. After all, this is just the application code. It should also be noted that Sound is sealed and defines an empty private constructor. These are just some details, the purpose is to make the user not erroneously derive class or create its instance from the Sound. The next feature of the code in Figure 1 is that the actual external method of the P / Invoke appears is the private method of SOUND. This method is only indirectly disclosed by the public MessageBeep method, and the latter accepts the parameters of the BeepTypes type. This indirect extra layer is a very critical detail that provides the following benefits. First, a future BEEP hosted method should be introduced in the class library, which can be repeatedly used by the public MessageBeep method, without having to change the rest of the application in the application.

The second advantage of the packaging method is that when you perform a P / Invoke call, you abandon the right to access conflicts and other low-level damage, which is usually provided by the CLR. The buffering method can protect the rest of your application from access conflicts and similar problems (even if it does not do anything but only pass parameters). This buffer method will be localized by any potential errors that are introduced by the P / Invoke.

Third, the third way to hide the private packaging is also the last advantage that the opportunity to add some minimum CLR styles to this method. For example, in FIG. 1, I convert the Boolean that the Windows API function fails to more like CLR. I also defined an enumeration type named BeepType, which corresponds to the definition value used with the Windows API. Since the C # does not support the definition, you can use the managed enumeration type to avoid spread to the entire application code.

The last benefit of the packaging method is for simple Windows API functions (such as MessageBeep). But when you start calling more complex non-hosting functions, you will find that manually convert Windows API styles into the benefits of more friendly methods to CLR more friendly methods will be more and more. The more intended to reuse the Interop function across the app, the more you should consider the design of the package. At the same time, I think that the use of the customer's parameters in the static packaging method of non-face-oriented objects is not.

DLL Import attribute

It is time to discuss more deeply. DLLIMPORTATITRIBUTE types play an important role when the hosted code is called P / INVOKE. The main role of DllimportAttribute is to give CLR to indicate which DLL exports the function you want to call. The name of the associated DLL is passed to the DLLIMPORTATITRIBUTE as a constructor parameter.

If you can't definite which DLL defines the Windows API function you want to use, the Platform SDK document will provide you with the best help resources. At the end of the Windows API function subject text, the SDK document specifies that the C application must use the function that the function must be linked. LIB file. In almost all cases, the .lib file has the same name as the system DLL file defined the function. For example, if the function requires a C application link to kernel32.lib, the function is defined in the kernel32.dll. You can find Platform SDK document topics about MessageBeep in MessageBeep. At the end of this topic, you will notice that it indicates that the library file is user32.lib; this indicates that MessageBeep is exported from user32.dll. Optional DLLIMPORTATTRIBUTE attribute

In addition to pointed out of the host DLL, DllimportAttribute also contains some optional properties, four particular interesting: entrypoint, charset, setLastError and CallingConvention.

EntryPoint can set the attribute to indicate the entry point name of the exported DLL function in the case where the same name is derived from the DLL. This is especially useful when you define two ways to call the same non-hosting function. In addition, in Windows, you can also bind to the exported DLL function through their serial number values. If you need this, the ENTRYPOINT value such as "# 1" or "# 129" indicates the serial number value of the non-managed function in the DLL instead of the function name.

Charset For character sets, not all versions of Windows are also created. The Windows 9x series products are missing important Unicode support, while the Windows NT and Windows CE series use Unicode. The CLR running on these operating systems uses the unicode for internal representation of String and CHAR data. But don't worry - When the Windows 9X API function is called, the CLR will automatically perform the necessary conversions, convert it from Unicode to ANSI.

If the DLL function does not process text in any way, you can ignore the CHARSET property of the DLLIMPORTATTRIBUTE. However, when Char or String data is part of the equation, the charset property should be set to Charset.auto. This allows the CLR to use the appropriate character set according to the host OS. If the CHARSET property is not explicitly set, its default value is charset.ansi. This default is a disadvantage because of an interope call called on Windows 2000, Windows XP, and Windows NT®, it will negatively affect the performance of text parameters.

CHARSET.ANSI or CHARSET.UNICODE should be explicitly selected rather than using charset.auto is: You explicitly specify an export function, and the function is specific to some of these two Win32 OS. . The READIRECTORYCHANGESW API function is such an example, which only exists in a Windows NT-based operating system and only supports Unicode; in which case you should explicitly use Charset. Enicode.

Sometimes, whether the Windows API has a character set relationship is not obvious. A confirmation method that will never be wrong is to check the C language header file of the function in the Platform SDK. (If you are unable to see which header files you want to see, you can view the header files of each API function listed in the Platform SDK document.) If you find that the API function is indeed defined as a function name that maps to A or W Macro, the character set is related to the function you try to call. An example of a Windows API function is a GetMessage API declared in Winuser.h, you might be surprised to find that it has two versions of A and W. SetLASTERROR error handling is very important, but it is often forgotten when programming. When you perform P / Invoke call, it will face other challenges - handling the difference between Windows API error handling and exceptions in managed code. I can give you a suggestion.

If you are calling the Windows API function using P / Invoke, for this function, you use getLastError to find extension error messages, you should set the setLastError property to true in the DLLIMPORTATTRIBUTE of the external method. This applies to most external methods.

This causes the CLR to cache errors set by the API function after each time the external method is called. Then, in the packaging method, you can get the caching error value by calling the Marshal.getLastWin32ERROR method defined in the System.Runtime.InteropServices.Marshal type of the class library. My suggestion is to check these expectations from an error value from the API function and trigger these values ​​a perceived exception. For all other failures (including the unrecognized failure situation), the win32exception defined in the System.comPonentModel namespace is triggered, and the value returned by Marshal.GetlastWin32Error is passed to it. If you look back in the code in Figure 1, you will see this method in the public packaging of the Extern MessageBeep method.

CallingConvention I will introduce this will also be the least least a DLLIMPORTATTRIBUTE attribute is CallingConvention. Through this property, you can indicate which function call should be used to convent the parameters in the stack. The default value of Callingconvention.winaPi is the best choice, which is feasible in most cases. However, if the call does not work, you can check the declaration header file in the Platform SDK to see if the API function you call is an exception API that does not meet the invoking agreement.

Typically, the call convention for this machine function (such as Windows API functions or C-rule DLL functions) describes how to push parameters into a thread stack or from a thread stack. Most Windows API functions are first pushing the last parameters of the function into the stack, and then by the called function is responsible for cleaning the stack. In contrast, many C-runtime DLL functions are defined to push it into the stack in the order in which the method parameters appear in the method signature, and give the stack cleanup work to the caller.

Fortunately, let the P / Invoke call work only need to make the peripheral device to understand the call. Typically, it is the best choice from the default calingconvention.winapi. Then, in the C run DLL function and a few functions, it may be necessary to change the agreement to CallingConvention.cdecl.

Data package processing

Data package processing is a challenging aspect of P / Invoke. When data is transmitted between managers and unmanaged codes, the CLR follows many rules, and few developers will often encounter them until they can remember these rules. Unless you are a class library developer, there is no need to master the details in usual. In order to use P / Invoke to the most effectively on the CLR, even occasionally need Interop application developers should still understand some basic knowledge of data package processing. In the remainder of this month, I will discuss data encapsulation processing of simple numbers and string data. I will start from the most basic digital data package, and then introduce simple pointer to seal processing and string sealing processing.

Seale numbers and logic scales

Most of the Windows OS is written in c. Therefore, the data type used by the Windows API is either a C type, or it is either by type definition or macro definition. Let's take a look at data encapsulation processing without a pointer. Simple, first focus on the numbers and Boolean values.

When passing the parameters to the Windows API function, you need to know the answer to the following questions:

• Data is fundamentally integrated or floating-point? • If the data is integer, it is a symbol or no symbol? • If the data is intellectual, how is its number of bits? • If the data is a floating point, it is a single precision or double precision?

Sometimes the answer is obvious, but sometimes it is not obvious. The Windows API redefines the basic C data type in a variety of ways. Figure 2 lists some public data types of C and Win32 and its specifications, and a common language run library type with matching specifications.

Typically, as long as you select a CLR type that matches the Win32 type of the parameter, your code will work properly. However, there are also some special cases. For example, the BOOL type defined in the Windows API is a 32-bit integer. However, BOOL is used to indicate the Boolean value true or false. Although you don't have to deliver the BOOL parameter as the System.InT32 value, you will get a more suitable mapping if you use the System.Boolean type. Character type mapping is similar to BOOL because there is a specific CLR type (system.char) indicates the meaning of characters.

After understanding this information, step-by-step examples may be helpful. It is still used as an example, let's try the kernel32.dll low-level beep, which will be beep through the computer's speaker. The Platform SDK document of this method can be found in Beep. This machine API is recorded as follows:

Bool beep

DWORD DWFREQ, // Frequency

DWORD DWDURATION / / DURATION IN MILLISECONDS

);

In terms of parameter sealing processing, your job is to understand what CLR data type is compatible with the DWORD and BOOL data types used by the Beep API function. Review the chart in Figure 2, you will see DWORD is a 32-bit unsigned integer value, like the CLR type system.uint32. This means you can use the UINT32 value as two parameters sent to the BEEP. Bool return value is a very interesting situation because the chart tells us that in Win32, Bool is a 32-bit sign integer. Therefore, you can use the System.Int32 value as a return value from the BEEP. However, the CLR also defines the SYSTEM.BOOLEAN type as the semantics of the Boolean value, so it should be used instead. The CLR defaults to the SYSTEM.BOOLEAN value to 32-bit symbolic integers. The external method shown here is the result of the result of Beep P / Invoke method: [DLLIMPORT ("kernel32.dll", setLastError = true)]

Static Extern Boolean Beep

UINT32 FREQUENCY, UINT32 DURATION;

Pointer parameters

Many Windows API functions use a pointer as one or more parameters. The pointer adds the complexity of the encapsulated data because they add an indirect layer. If there is no pointer, you can pass the data over the thread stack. With a pointer, you can pass data by reference to pushing the memory address of the data into the thread stack. The function then accesss the data over the memory address. There are a variety of ways using managed code indicating that this additional indirect layer has multiple.

In C #, if the method parameters are defined as REF or OUT, the data is passed by reference instead of via value. This is true even if you don't use Intero, but just call to another hosted method from a managed method. For example, if the System.Int32 parameter is passed through the REF, the address of the data is transmitted in the thread stack, not an integer value itself. Below is an example of a method defined as a receiving integer value by reference:

Void flipint32 (Ref int32 num) {

Num = -num;

}

Here, the FLIPINT32 method obtains an address of an int32 value, access data, reverses it, and then assigns a reversible value to the original variable. In the following code, the FLIPINT32 method changes the value of the call to the call from 10 to -10:

INT32 x = 10;

FLIPINT32 (Ref x);

This capability can be reused in the hosted code, and pass the pointer to the non-hosting code. For example, the FileEncryptionStatus API function returns file encryption status in a 32-bit unsigned bit mask. This API is recorded in the following manner:

Bool FileEncryptionStatus

LPCTSTR LPFILENAME, / / ​​FILE NAME

LPDWORD LPSTATUS / / ENCRYPTION STATUS

);

Note that this function does not return status, but returns a boolean value, indicating whether the call is successful. In the case of success, the actual status value is returned by the second parameter. It works to call the program to pass the function to the function to a pointer to a DWORD variable, and the API function is filled with the pointing memory location. The following code snippet shows a possible external method for calling the unmanaged FileEncryptionStatus function:

[DLLIMPORT ("Advapi32.dll", Charset = Charset.Auto] Static Extern Boolean FileEncryptionStatus (String FileName,

Out uint32 status;

This definition uses the OUT keyword to indicate the BY-REF parameter for the UINT32 status value. Here I can also select the REF keyword, which actually produces the same machine code at runtime. The OUT key is just a specification of a BY-REF parameter, which indicates that the passed data is only passed outside the data being called to the C # compiler. Conversely, if the REF keyword is used, the compiler assumes that the data can be passed inside and outside the function being called.

Another good aspect of the OUT and REF parameters in the hosted code is that the address as the BY-REF parameter can be an element of a local variable, a class or structure in a thread stack, or an appropriate data type. An element reference in an array. This flexibility of the calling program makes the BY-REF parameter a good starting point for the encapsulated buffer pointer and the single value pointer. Only if I found that REF or OUT parameters do not meet my needs, I will consider encapsulating the pointer to a more complex CLR type (such as class or array object).

If you are not familiar with C grammar or call Windows API functions, it is sometimes difficult to know if a method parameter needs a pointer. A common indicator is to see if the parameter type is starting with letters P or LP, such as LPDWORD or PINT. In both cases, the LP and P indicate parameters are a pointer, and their point to which they point to DWORD or INT, respectively. However, in some cases, you can directly use an asterisk (*) in the C language syntax as a pointer. The following code snippet shows this example:

Void Takesapointer (DWORD * PNUM);

It can be seen that the only parameter of the above functions is a pointer to the DWORD variable.

When the pointer is encapsulated by the P / Invoke, REF and OUT are only used in the value type in the hosted code. When the CLR type of a parameter uses the Struct key definition, it is considered that this parameter is a value type. OUT and REF are used to block pointers that point to these data types because the usual value type variable is object or data, and there is no reference to the value type in the managed code. Conversely, when the reference type object is encapsulated, the REF and OUT keywords are not required, as variables are already referenced by the object.

If you are not very familiar with the difference between the reference type and value type, please refer to MSDN® Magazine issued in December 2000. You can find more information in the .NET column. Most CLR types are reference types; however, in addition to System.String and System.Object, all primitive types (such as System.Int32 and System.Boolean are all value types.

Get an opaque pointer: a special case

Sometimes the pointer passed or returned in the Windows API is opaque, which means that the pointer value is a pointer from the technical perspective, but the code does not use it directly. Instead, the code returns the pointer to Windows for later reuse.

A very common example is the concept of handle. In Windows, the internal data structure (button from the file to the screen) is represented in the application code as the handle. The handle is actually an opaque pointer or a value with a pointer width, the application uses it to represent the internal OS structure.

In a small number, the API function also defines the opaque pointer as a PVOID or LPVOID type. In the definition of the Windows API, these types means that this pointer is not type. When an opaque pointer returns to your application (or your application expects to get an opaque pointer), you should seal the parameter or return value as a special type in the CLR - System.intptr. When you use the intPtr type, you usually do not use the OUT or REF parameters because INTPTR is to hold a pointer directly. However, if you get a pointer to a pointer, it is appropriate to use the BY-REF parameter for INTPTR.

In the CLR type system, the System.intptr type has a special property. Unlike other base types in the system, INTPTR does not have a fixed size. Instead, it is based on the size of the runtime, which is determined by the normal pointer of the operating system. This means that in 32-bit Windows, the INTPTR variable has a width of 32 bits, while in the 64-bit Windows, the real-time compiler compiles the INTPTR value as a 64-bit value. This automatic adjustment size is very useful when encourse the opaque pointer between the managed code and the non-hosting code.

Remember, any API function returned or accepted by the handle is actually an opaque pointer. Your code should be encapsulated in Windows into the System.InTPTR value.

You can convert the INTPTR value to a 32-bit or 64-bit integer value in the managed code, or to force the latter to force to the former. However, when using the Windows API function, because the pointer should be opaque, it cannot be used in addition to the storage and delivery to the external method. Two special examples of this "SWD and transfer" rules are when you need to pass NULL pointer values ​​to external methods and need to compare the INTPTR value and NULL value. To do this, you cannot convert zero to System.intptr, and should use int32.zero static public fields on the intPtr type to obtain a NULL value for comparison or assignment.

Seal text

Text data is often processed during programming. Text has created some troubles for Interop, which has two reasons. First, the underlying operating system may use Unicode to represent a string, or ANSI can be used. In a very small number of cases, the two parameters of the MultibyToWideChar API function are inconsistent on the character set.

The second reason is that when P / INVOKE needs to be made, it is necessary to deal with the text that C and CLR processing text is different. In C, the string is actually just a character value, usually as a null. Most Windows API functions processes strings according to the following conditions: For ANSI, it uses it as a character value array; for Unicode, it is used as a wide character value.

Fortunately, the CLR is designed to be quite flexible, and when the text is blocked, the problem is easily resolved, not what the Windows API function expects to get from your application. Here is some main considerations that need to be remembered:

• Is your application passed text data to the API function, or the API function returns string data to your application? Or both? • What kind of managed type should you use? • What is the format of a non-hosting string for API functions?

We first answer the last problem. Most Windows API functions have LPTSTR or LPCTSTR values. (From the perspective of function) They are modified and unmodified buffers, including an array of characters ended with NULL. "C" represents a constant, meaning that the parameter information will not be passed to the outside of the function. "T" in LPTSTR indicates that the parameter can be Unicode or ANSI depends on the character set you selected and the base set of the underlying operating system. Because most of the number string parameters in the Windows API are one of these two types, as long as charset.auto is selected in DllimportAttribute, the CLR works in the default manner. However, some API functions or custom DLL functions represent a string in different ways. If you want to use a such function, you can use the string parameters of the MarshalasAttribute to modify the external method and indicate a string format that is different from the default LPTSTR. For more information on MarshalasAttribute, see the Platform SDK document topic in Marshalasattribute Class.

Let's take a look at the direction of the string information to pass between your code and the non-hosting function. There are two ways to know the direction of delivery of information when the string is handled. The first is also the most reliable method is the first to understand the use of parameters. For example, you positively use a parameter, its name is similar to createmutex, with a string, you can imagine that the string information is passed from the application to the API function. At the same time, if you call getUserName, the name of the function indicates that the string information is passed from the function to your application.

In addition to this relatively reasonable method, the second way to find the information transfer direction is to find the letters "C" in the API parameter type. For example, the first parameter of the GetUserName API function is defined as the LPTSTR type, which represents a long pointer pointing to the Unicode or ANSI string buffer. However, the name parameters of Createmutex are typed to LTCTSTR. Note that the type definition here is the same, but adds a letter "C" to indicate that the buffer is constant, and the API function cannot be written.

Once the text parameters are specifically used as input or used as input / output, it can determine which CLR type used as a parameter type. There are some rules here. If the string parameter is only used as input, use the System.String type. In the hosted code, the string is constant, suitable for buffers that are not changed by the native API function.

If the string parameter can be used as input and / or output, use the System.StringBuilder type. The StringBuilder type is a very useful class library type that helps you build a string effectively, or you can pass the buffer to the native function, which is fill the string data for you by this function. Once the function call returns, you only need to call the TString of the StringBuilder object to get a String object.

GetShortPathname API function can be used to show when using String, when using StringBuilder because it only has three parameters: an input string, an output string and a parameter indicating the character length of the output buffer.

Figure 3 shows the non-hosting GetShortPathName function documentation of the added release, which also points out the input and output string parameters. It introduces the user's definition, as shown in Figure 3. Note that the first parameter is sealed to System.String because it is only used as an input parameter. The second parameter represents an output buffer, which uses System.StringBuilder.

summary

The P / Invoke feature introduced in this month is enough to call many API functions in Windows. However, if you use the interop, you will eventually find a very complex data structure, and may even need to directly access memory through the pointer in the managed code. In fact, INTEROP in this machine code can be a real Pandora box that will be detailed and low-ranked inside. CLR, C # and hosted C provide many useful features; maybe I will introduce advanced P / Invoke topics in this column later. At the same time, as long as you feel that the .NET Framework class library cannot play your voice or perform other features for you, you can know how to seek some help from the original and excellent Windows API.

转载请注明原文地址:https://www.9cbs.com/read-130411.html

New Post(0)