Uniform access of error codes in unmanaged API

I am writing a wrapper around a rather large unmanaged API. Almost every imported method returns a common error code when it fails. While I'm doing this:

ErrorCode result = Api.Method(); if (result != ErrorCode.SUCCESS) { throw Helper.ErrorToException(result); } 

It works great. The problem is that I have so many unmanaged method calls that it becomes extremely frustrating and repetitive. So, I tried switching to this:

 public static void ApiCall(Func<ErrorCode> apiMethod) { ErrorCode result = apiMethod(); if (result != ErrorCode.SUCCESS) { throw Helper.ErrorToException(result); } } 

Lets me cut all these calls to one line:

 Helper.ApiCall(() => Api.Method()); 

However, there are two problems with this. Firstly, if my unmanaged method uses out parameters, I must first initialize the local variables, because the method call is actually in the delegate. I would just declare the assignment out without initializing it.

Secondly, if an exception is thrown, I really don't know where it came from. The debugger goes into the ApiCall method, and the stack trace shows only the method that contains the ApiCall call, not the delegate itself. Since I can have many API calls in one method, this makes debugging difficult.

Then I thought about using PostSharp to wrap all unmanaged calls by checking the error code, but I'm not sure how this could be done using extern methods. If it ends just by creating a wrapper method for each of them, then I would have the same problem with the exception as with the ApiCall method, right? Also, how would the debugger know how to show me the site of the generated exception in my code if it exists only in the assembly?

Then I tried to implement a custom marshaler that would intercept the return value of the API calls and check the error code there. Unfortunately, you cannot use your own marshaler to return values. But I think it would be a really clean solution if it worked.

 [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef=typeof(ApiMethod))] public static extern ErrorCode Method(); 

Now I have absolutely no ideas. What are some other ways I could handle this?

+4
source share
6 answers

Follow the ErrorHandler class from the Visual Studio 2010 SDK. It existed in earlier versions, but the new CallWithCOMConvention(Action) , which can be valuable depending on how your API interacts with other managed code.

Of the available methods, I recommend implementing the following:

  • Succeeded(int)
    ( Failed() simple !Succeeded() , so you can skip it)

  • ThrowOnFailure(int)
    (Throws the correct exception for your return code)

  • CallWith_MyErrorCode_Convention(Action) and CallWith_MyErrorCode_Convention(Func<int>)
    (e.g. CallWithCOMConvention , but for your error codes)

  • IsCriticalException(Exception)
    (using CallWith_MyErrorCode_Convention )

+1
source

What happens if you do not check ErrorCode.SUCCESS ? Will your code quickly fail and throw an exception? Can you tell which unmanaged API failed to execute if your code is managed? If so, consider not checking for errors and just letting run-time run when your unmanaged API crashes.

If this is not the case, I suggest biting the bullet and following your first idea. I know that you called it “disappointing and repetitive”, but after exiting the project with a “smart” macro solution to a similar problem, checking the return values ​​in method calls and exceptions for wrapping is the door to madness: exception messages and stack traces become introductory misleading, you cannot track code, performance suffers, your code becomes optimized for errors and goes off the rails after success.

If the specific return value is an error, then throw a unique exception. If this may not be a mistake, let it go and rush if it becomes a mistake. You said you want to reduce the check by one line?

 if (Api.Method() != ErrorCode.SUCCESS) throw new MyWrapperException("Api.Method broke because ..."); 

Your suggestion also raises the same exception if any method returns the same "common error code". This is another debugging nightmare; for APIs that return the same error codes from multiple calls, do the following:

 switch (int returnValue = Api.Method1()) { case ErrorCode.SUCCESS: break; case ErrorCode.TIMEOUT: throw new MyWrapperException("Api.Method1 timed out in situation 1."); case ErrorCode.MOONPHASE: throw new MyWrapperException("Api.Method1 broke because of the moon phase."); default: throw new MyWrapperException(string.Format("Api.Method1 returned {0}.", returnValue)); } switch (int returnValue = Api.Method2()) { case ErrorCode.SUCCESS: break; case ErrorCode.TIMEOUT: throw new MyWrapperException("Api.Method2 timed out in situation 2, which is different from situation 1."); case ErrorCode.MONDAY: throw new MyWrapperException("Api.Method2 broke because of Mondays."); default: throw new MyWrapperException(string.Format("Api.Method2 returned {0}.", returnValue)); } 

Verbose? Yeah. Disappointment? No, what is frustrating is the attempt to debug an application that throws the same exception from each line, regardless of the error.

+1
source

I think a simple way is to add an extra layer.

 class Api { .... private static ErrorCode Method();//changing Method to private public static void NewMethod()//NewMetod is void, because error is converted to exceptions { ErrorCode result = Method(); if (result != ErrorCode.SUCCESS) { throw Helper.ErrorToException(result); } } .... } 
0
source

Create a private property to hold the ErrorCode value and throw an exception from the installer.

 class Api { private static ErrorCode _result; private static ErrorCode Result { get { return _result; } set { _result = value; if (_result != ErrorCode.SUCCESS) { throw Helper.ErrorToException(_result); } } } public static void NewMethod() { Result = Api.Method(); Result = Api.Method2(); } } 
0
source

Write a T4 template to make a generation for you.

0
source

The existing code is actually really, really close. If you use an expression tree to store a lambda, not a Func delegate, then your Helper.ApiCall can pull out the identifier of the called function and add it to the exception that it selected. For more information on expression trees and some very good examples, Google Mark Gravell .

0
source

All Articles