After doing a little digging, it turns out that the directx error routines are not shipped as dlls, they're shipped as libs that you statically link into your executable. I should have figured this based on the size of dxerr9.lib (it's 5M, way larger than any of the other stub libs).
So to get this to work I did the following:
- Make a "wrapper" native dll that wraps all the error handling lib functions. Since there are only 3 (DXGetErrorDescription, DXGetERrorString and DXTrace) this isn't that big a deal.
- Have .NET call the wrapper functions.
You can do this in visual studio by creating a Win32 project (change application settings so that it's a dll and so that it exports symbols). I called min "DirectXErrWrapper".
Inside the wrapper project add a passthrough for the DXGetErrorDescription. I called mine DXGetErrDescPassThrough():
extern "C" {
DIRECTXERRWRAPPER_API TCHAR * DXGetErrDescPassThrough(HRESULT hr)
{
return (TCHAR *)DXGetErrorDescription(hr);
}
}
The extern "C" block is important (has to do with demangling the C++ names). This project will need to #include "dxerr.h" and statically link dxerr.lib so you'll have to add the directx sdk include dir to your include path and the directx sdk lib dir to your lib path.
Then have your managed code import the call from your wrapper dll:
[DllImport("DirectXErrWrapper.dll", CharSet=CharSet.Unicode)]
static public extern string DXGetErrDescPassThrough(int DXError);
In 2005 native projects use unicode by default so the DllImport needed to specify unicode. Lastly, call it from managed code via:
Debug.WriteLine("DXGettErrDescPassThrough=" + DXGetErrDescPassThrough(0));
In this case the call prints "The function completed successfully" to the debug device since 0 is success.
(previous post follows)
That looks like one of the DirectX functions (C++ not C# AFAIK). I'm pretty sure it ships with unicode and ansi versions.
TCHAR will map to unicode when compiled on unicode (the T is a mnemonic for type; unicode on unicode, multibyte characterset on mbcs, ansi otherwise).
You can usually use CharSet=CharSet.Auto but if that causes compatability problems (e.g., you've gotta use ansi strings because of some struct that you've already ported requires it) you can use whichever is consistent with your project (either Charset.Unicode if you're all unicode or Charset.Ansi) you should have no problem marshaling it into a C# string. You may need to decorate it as [OUT] though the return value may be OUT implicitly.
I too wish DirectX shipped w/ TLBs following the standard [out, retval] convention!
Contrary to the suggestions by some of the other answers, using the DllImport
attribute is still the correct approach.
I honestly don't understand why you can't do just like everyone else in the world and specify a relative path to your DLL. Yes, the path in which your application will be installed differs on different people's computers, but that's basically a universal rule when it comes to deployment. The DllImport
mechanism is designed with this in mind.
In fact, it isn't even DllImport
that handles it. It's the native Win32 DLL loading rules that govern things, regardless of whether you're using the handy managed wrappers (the P/Invoke marshaller just calls LoadLibrary
). Those rules are enumerated in great detail here, but the important ones are excerpted here:
Before the system searches for a DLL, it checks the following:
- If a DLL with the same module name is already loaded in memory, the system uses the loaded DLL, no matter which directory it is in. The system does not search for the DLL.
- If the DLL is on the list of known DLLs for the version of Windows on which the application is running, the system uses its copy of the known DLL (and the known DLL's dependent DLLs, if any). The system does not search for the DLL.
If SafeDllSearchMode
is enabled (the default), the search order is as follows:
- The directory from which the application loaded.
- The system directory. Use the
GetSystemDirectory
function to get the path of this directory.
- The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
- The Windows directory. Use the
GetWindowsDirectory
function to get the path of this directory.
- The current directory.
- The directories that are listed in the
PATH
environment variable. Note that this does not include the per-application path specified by the App Paths registry key. The App Paths key is not used when computing the DLL search path.
So, unless you're naming your DLL the same thing as a system DLL (which you should obviously not be doing, ever, under any circumstances), the default search order will start looking in the directory from which your application was loaded. If you place the DLL there during the install, it will be found. All of the complicated problems go away if you just use relative paths.
Just write:
[DllImport("MyAppDll.dll")] // relative path; just give the DLL's name
static extern bool MyGreatFunction(int myFirstParam, int mySecondParam);
But if that doesn't work for whatever reason, and you need to force the application to look in a different directory for the DLL, you can modify the default search path using the SetDllDirectory
function.
Note that, as per the documentation:
After calling SetDllDirectory
, the standard DLL search path is:
- The directory from which the application loaded.
- The directory specified by the
lpPathName
parameter.
- The system directory. Use the
GetSystemDirectory
function to get the path of this directory.
- The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
- The Windows directory. Use the
GetWindowsDirectory
function to get the path of this directory.
- The directories that are listed in the
PATH
environment variable.
So as long as you call this function before you call the function imported from the DLL for the first time, you can modify the default search path used to locate DLLs. The benefit, of course, is that you can pass a dynamic value to this function that is computed at run-time. That isn't possible with the DllImport
attribute, so you will still use a relative path (the name of the DLL only) there, and rely on the new search order to find it for you.
You'll have to P/Invoke this function. The declaration looks like this:
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool SetDllDirectory(string lpPathName);
Best Solution
Win32 functions almost never return a
HRESULT
. Instead they return aBOOL
or use special values to indicate error (e.g.CreateFile
returnsINVALID_HANDLE_VALUE
). They store the error code in a per-thread variable, which you can read withGetLastError()
.SetLastError=true
instructs the marshaler to read this variable after the native function returns, and stash the error code where you can later read it withMarshal.GetLastWin32Error()
. The idea is that the .NET runtime may call other Win32 functions behind the scenes which mess up the error code from your p/invoke call before you get a chance to inspect it.Functions which return a
HRESULT
(or equivalent, e.g.NTSTATUS
) belong to a different level of abstraction than Win32 functions. Generally these functions are COM-related (above Win32) or fromntdll
(below Win32), so they don't use the Win32 last-error code (they might call Win32 functions internally, though).PreserveSig=false
instructs the marshaler to check the returnHRESULT
and if it's not a success code, to create and throw an exception containing theHRESULT
. The managed declaration of yourDllImport
ed function then hasvoid
as its return type.Remember, the C# or VB compiler cannot check the
DllImport
ed function's unmanaged signature, so it has to trust whatever you tell it. If you putPreserveSig=false
on a function which returns something other than aHRESULT
, you will get strange results (e.g. random exceptions). If you putSetLastError=true
on a function which does not set the last Win32 error code, you will get garbage instead of a useful error code.