统一处理非托管 API 中的错误代码

发布于 2024-08-14 17:21:16 字数 1348 浏览 6 评论 0原文

我正在围绕一个相当大的非托管 API 编写一个包装器。几乎每个导入的方法在失败时都会返回一个常见的错误代码。现在,我正在这样做:

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

效果很好。问题是,我有很多非托管方法调用,这变得非常令人沮丧和重复。因此,我尝试切换到此:

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

这使我可以将所有这些呼叫减少到一条线路:

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

但是,这有两个直接问题。首先,如果我的非托管方法使用 out 参数,我必须首先初始化局部变量,因为方法调用实际上是在委托中。我希望能够简单地声明一个 out 目的地而不对其进行初始化。

其次,如果抛出异常,我真的不知道它是从哪里来的。调试器跳转到 ApiCall 方法,堆栈跟踪仅显示包含对 ApiCall 的调用的方法,而不是委托本身。由于我可以在一个方法中进行许多 API 调用,这使得调试变得困难。

然后,我考虑使用 PostSharp 通过错误代码检查来包装所有非托管调用,但我不确定如何使用 extern 方法来完成此操作。如果它最终只是为每个方法创建一个包装器方法,那么我将遇到与 ApiCall 方法相同的异常问题,对吗?另外,如果调试器只存在于已编译的程序集中,调试器如何知道如何向我显示代码中引发异常的位置?

接下来,我尝试实现一个自定义封送拆收器,该封送拆收器将拦截 API 调用的返回值并检查那里的错误代码。不幸的是,您无法应用自定义封送拆收器来返回值。但我认为,如果可行的话,这将是一个非常干净的解决方案。

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

现在我完全没有想法了。我还可以通过哪些其他方法来处理这个问题?

I'm writing a wrapper around a fairly large unmanaged API. Almost every imported method returns a common error code when it fails. For now, I'm doing this:

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

This works fine. The problem is, I have so many unmanaged method calls that this gets 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);
    }
}

Which allows me to cut down all of those calls to one line:

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

There are two immediate problems with this, however. First, if my unmanaged method makes use of out parameters, I have to initialize the local variables first because the method call is actually in a delegate. I would like to be able to simply declare a out destination without initializing it.

Second, if an exception is thrown, I really have no idea where it came from. The debugger jumps into the ApiCall method and the stack trace only shows the method that contains the call to ApiCall rather than the delegate itself. Since I could have many API calls in a single method, this makes debugging difficult.

I then thought about using PostSharp to wrap all of the unmanaged calls with the error code check, but I'm not sure how that would be done with extern methods. If it ends up simply creating a wrapper method for each of them, then I would have the same exception problem as with the ApiCall method, right? Plus, how would the debugger know how to show me the site of the thrown exception in my code if it only exists in the compiled assembly?

Next, I tried implementing a custom marshaler that would intercept the return value of the API calls and check the error code there. Unfortunately, you can't apply a custom marshaler to return values. But I think that would have been a really clean solution it if had worked.

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

Now I'm completely out of ideas. What are some other ways that I could handle this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

又怨 2024-08-21 17:21:16

遵循 ErrorHandler来自 Visual Studio 2010 SDK 的 类。它存在于早期版本中,但新版本具有 CallWithCOMConvention(Action),这可能很有价值,具体取决于您的 API 与其他托管代码交互的方式。

在可用的方法中,我建议实现以下方法:

  • Succeeded(int)
    Failed() 只是 !Succeeded(),因此您可以跳过它)

  • ThrowOnFailure(int)
    (为您的返回代码引发适当的异常)

  • CallWith_MyErrorCode_Convention(Action)CallWith_MyErrorCode_Convention(Func)
    (类似于 CallWithCOMConvention,但用于您的错误代码)

  • IsCriticalException(Exception)
    (由CallWith_MyErrorCode_Convention使用)

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

Of the available methods, I recommend implementing the following:

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

  • ThrowOnFailure(int)
    (Throws a proper exception for your return code)

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

  • IsCriticalException(Exception)
    (used by CallWith_MyErrorCode_Convention)

魂牵梦绕锁你心扉 2024-08-21 17:21:16

如果您不检查 ErrorCode.SUCCESS 会发生什么?您的代码会很快失败并抛出异常吗?如果您的托管代码抛出异常,您能否判断出哪个非托管 API 失败了?如果是这样,请考虑不检查错误,而只是在非托管 API 失败时让运行时抛出异常。

如果情况并非如此,我建议硬着头皮遵循你的第一个想法。我知道你称其为“令人沮丧和重复”,但是在来自一个对类似问题有“聪明”宏解决方案的项目之后,检查方法调用中的返回值并包装异常是通往疯狂的大门:异常消息和堆栈跟踪变得误导,您无法跟踪代码,性能受到影响,您的代码针对错误进行了优化,并在成功后偏离了轨道。

如果特定返回值是错误,则抛出唯一的异常。如果它可能不是一个错误,就让它过去,如果变成一个错误则抛出。你说你想把检查减少到一行?

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

如果任何方法返回相同的“常见错误代码”,您的提案也会引发相同的异常。这是另一个调试噩梦;对于从多次调用返回相同错误代码的 API,请执行以下操作:

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's 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));
}

详细?是的。令人沮丧?不,令人沮丧的是尝试调试一个应用程序,无论出现什么错误,该应用程序都会从​​每一行抛出相同的异常。

What happens if you don't check ErrorCode.SUCCESS? Will your code quickly fail and throw an exception? Can you tell which unmanaged API failed if your managed code throws? If so, consider not checking for errors and just letting the runtime throw when your unmanaged API fails.

If this is not the case, I suggest biting the bullet and following your first idea. I know you called it "frustrating and repetitive", but after coming from a project with a "clever" macro solution to a similar problem, checking return values in method calls and wrapping exceptions is the doorway to insanity: exception messages and stack traces become misleading, you can't trace the code, performance suffers, your code become optimized for errors and goes off the rails upon success.

If a particular return value is an error, thow a unique exception then. If it might not be an error, let it go and throw if becomes an error. You said you wanted to reduce the check to one line?

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

Your proposal also throws the same exception if any method returns the same "common error code". This is another debugging nightmare; for APIs which return the same error codes from multiple calls, do this:

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's 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? Yup. Frustrating? No, what's frustrating is trying to debug an app that throws the same exception from every line whatever the error.

难如初 2024-08-21 17:21:16

我认为,简单的方法是添加额外的层。

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);
  }

  }
....
}

I think, the easy way is to add aditional 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);
  }

  }
....
}
陌上芳菲 2024-08-21 17:21:16

创建一个私有属性来保存 ErrorCode 值,并从 setter 中抛出异常。

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();
    }
}

Create a private property to hold the ErrorCode value, and throw the exception from the setter.

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();
    }
}
雪若未夕 2024-08-21 17:21:16

编写一个 T4 模板来为您进行生成。

Write a T4 template to do the generation for you.

最美的太阳 2024-08-21 17:21:16

您现有的代码实际上非常非常接近。如果您使用表达式树来保存 lambda,而不是 Func 委托,那么您的 Helper.ApiCall 可以提取被调用函数的标识并将其添加到它引发的异常中。有关表达式树的更多信息和一些非常好的示例,请 Google 马克·格拉维尔

Your existing code is actually really, really close. If you use an expression tree to hold the lambda, instead of a Func delegate, then your Helper.ApiCall can pull out the identity of the function that was called and add that to the exception it throws. For more information on expression trees and some very good examples, Google Marc Gravell.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文