【问题标题】:WinApi - GetLastError vs. Marshal.GetLastWin32ErrorWinApi - GetLastError 与 Marshal.GetLastWin32Error
【发布时间】:2013-07-28 21:39:24
【问题描述】:

我测试了很多。但我发现这 2 个没有缺点!
但请参阅接受的答案。


我读到here 在托管代码中调用GetLastError 是不安全的,因为框架可能在内部“覆盖”最后一个错误。我对GetLastError 从来没有任何明显的问题,而且在我看来,.NET Framework 足够聪明,不会覆盖它。因此,我对该主题有几个问题:
  • [DllImport("kernel32.dll", SetLastError = true)] 中的SetLastError 属性是否使框架存储错误代码以供Marshal.GetLastWin32Error() 使用?
  • 有没有简单的GetLastError 无法给出正确结果的例子?
  • 真的必须使用Marshal.GetLastWin32Error()吗?
  • 这个“问题”框架版本是否相关?

public class ForceFailure
{
    [DllImport("kernel32.dll")]
    static extern uint GetLastError();
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool SetVolumeLabel(string lpRootPathName, string lpVolumeName);

    public static void Main()
    {
        if (SetVolumeLabel("XYZ:\\", "My Imaginary Drive "))
            System.Console.WriteLine("It worked???");
        else
        {
            // the first last error check is fine here:
            System.Console.WriteLine(GetLastError());
            System.Console.WriteLine(Marshal.GetLastWin32Error());
        }
    }
}


产生错误:
if (SetVolumeLabel("XYZ:\\", "My Imaginary Drive "))
    Console.WriteLine("It worked???");
else
{
    // bad programming but ok GetlLastError is overwritten:
    Console.WriteLine(Marshal.GetLastWin32Error());
    try
    {
        using (new FileStream("sdsdafsdfsdfs sdsd ", FileMode.Open)) { }
    }
    catch { }
    Console.WriteLine(GetLastError());
}

if (SetVolumeLabel("XYZ:\\", "My Imaginary Drive "))
    Console.WriteLine("It worked???");
else
{
    // bad programming and Marshal.GetLastWin32Error() is overwritten as well:
    Console.WriteLine(GetLastError());
    try
    {
        using (new FileStream("sdsdafsdfsdfs sdsd ", FileMode.Open)) { }
    }
    catch { }
    Console.WriteLine(Marshal.GetLastWin32Error());
}

// turn off concurrent GC
GC.Collect(); // doesn't effect any of the candidates

Console.WriteLine(" -> " + GetLastError());
Console.WriteLine(" -> " + GetLastError());
Console.WriteLine(Marshal.GetLastWin32Error());
Console.WriteLine(Marshal.GetLastWin32Error());
// when you exchange them -> same behaviour just turned around

我看不出有什么不同!除了Marshal.GetLastWin32Error 也存储来自 App->CLR->WinApi 调用的结果和GetLastError 仅存储来自 App->WinApi 调用的结果之外,两者的行为相同。


垃圾收集似乎没有调用任何覆盖最后一个错误代码的 WinApi 函数
  • GetLastError 是线程安全的。 SetLastError 为调用它的每个线程存储一个错误代码。
  • GC 什么时候会在我的线程中运行?

【问题讨论】:

  • GetLastError 有效,它可能适用于所有现有的 .NET Framework 版本和实现。因此,您的代码正在运行,但这并不能证明任何事情。 .NET Framework 开发人员可以随意更改 .NET 实现,GetLastError 有一天会停止工作。
  • .NET 开发人员只保证GetLastWin32Error 工作正常。你想使用GetlastError hack,它可能总是有效,但这仍然是hack。所以,这个问题有点哲学:如果没有证明,我们可以使用 hacks,它是不正确的。
  • @AlexFarber 那将是一个糟糕的 .NET Framework 版本。我可以想象很多软件都使用简单的 GetLastError 运行,因为程序员还没有听说过 Marshal.GetLastWin32Error 或其他什么。进行您提到的更新会破坏所有这些软件。
  • @AlexFarber 见this。我想确切地知道我是否必须或不必通过现有代码中对 GetLastError 的所有使用。目前还没有证据证明它是不安全的。
  • GetLastError() 是线程安全的,来自 Microsoft 的信息:“最后一个错误代码是在每个线程的基础上维护的。多个线程不会覆盖彼此的最后一个错误代码”。

标签: c# winapi marshalling unmanaged managed


【解决方案1】:

您必须始终使用Marshal.GetLastWin32Error。主要问题是垃圾收集器。如果它在SetVolumeLabel 的调用和GetLastError 的调用之间运行,那么您将收到错误的值,因为 GC 肯定会覆盖最后的结果。

因此,您始终需要在DllImport-属性中指定SetLastError=true

[DllImport("kernel32.dll", SetLastError=true)]
static extern bool SetVolumeLabel(string lpRootPathName, string lpVolumeName);

这确保 marhsallling 存根在本地函数“GetLastError”之后立即调用并将其存储在本地线程中。

如果您已指定此属性,则对Marshal.GetLastWin32Error 的调用将始终具有正确的值。

有关更多信息,另请参阅 Adam Nathan 的 "GetLastError and managed code"

.NET 中的其他函数也可以更改窗口“GetLastError”。这是一个产生不同结果的示例:

using System.IO;
using System.Runtime.InteropServices;

public class ForceFailure
{
  [DllImport("kernel32.dll")]
  public static extern uint GetLastError();

  [DllImport("kernel32.dll", SetLastError = true)]
  private static extern bool SetVolumeLabel(string lpRootPathName, string lpVolumeName);

  public static void Main()
  {
    if (SetVolumeLabel("XYZ:\\", "My Imaginary Drive "))
      System.Console.WriteLine("It worked???");
    else
    {
      System.Console.WriteLine(Marshal.GetLastWin32Error());
      try
      {
        using (new FileStream("sdsdafsdfsdfs sdsd ", FileMode.Open)) {}
      }
      catch
      {
      }
      System.Console.WriteLine(GetLastError());
    }
  }
}

另外,这似乎取决于您使用的 CLR!如果你用 .NET2 编译它,它会产生“2 / 0”;如果您切换到 .NET 4,它将输出“2 / 2”...

所以它依赖于CLR版本,但你不应该相信原生的GetLastError函数;始终使用Marshal.GetLastWin32Error

【讨论】:

  • 我无法创建由GetLastError 返回的错误值。请参阅我的编辑和示例代码。
  • 好吧,你的例子有点证明你错了。如果您交换它们,您会从Marshal.GetLastWin32Error 得到相同的不需要的行为。但这实际上是糟糕的编程。就像您覆盖一个变量并期望它仍然具有旧值一样。据我了解,dllimport 的东西在 .NET 中设计得很好,使用 GetLastError 是省钱的,尽管人们试图吓唬你不要使用它。
  • 你的答案应该被接受为正确的答案。 GetLastError 文章令人大开眼界 :) 谢谢
  • 我同意@Bitterblue 的观点,即该示例至少是错误的或具有误导性。据推测,新的 FileStream 要么产生新的错误,要么将其重置。预计 Marshal.GetLastWin32Error 和 GetLastError 返回的结果都返回“新”结果。
  • 嗨@BatteryBackupUnit:该函数只是在那里,要指出的是,我们不知道在代码的非托管和托管部分之间可能调用哪个Win32 API......我们没有写CLR,所以我们需要假设可能有其他 Win32 API 调用是我们无法控制的一些特殊情况(如 GC 或其他场景)。因此我们需要使用“SetLastError”来确保我们得到正确的错误代码。
【解决方案2】:

TL;DR

  • 请使用[DllImport(SetLastError = true)]Marshal.GetLastWin32Error()
  • Win32 调用失败后立即在同一线程上执行Marshal.GetLastWin32Error()

论证

在我阅读时,官方解释为什么需要Marshal.GetLastWin32Error可以找到here

公共语言运行时可以对覆盖操作系统维护的 GetLastError 的 API 进行内部调用。

换句话说:

在设置错误的 Win32 调用之间,CLR 可能会“插入”其他可能覆盖错误的 Win32 调用。 指定 [DllImport(SetLastError = true)] 可确保 CLR 在 CLR 执行任何意外的 Win32 调用之前检索错误代码。 要访问该变量,我们需要使用Marshal.GetLastWin32Error

现在@Bitterblue 发现这些“插入的调用”并不经常发生——他找不到。但这并不令人惊讶。为什么?因为“黑盒测试”GetLastError 是否可靠工作非常困难:

  • 只有在插入 CLR 的 Win32 调用实际上失败时,您才能检测到不可靠性。
  • 这些调用的失败可能取决于内部/外部因素。比如时间/时序、内存压力、设备、电脑状态、windows版本……
  • CLR 插入 Win32 调用可能取决于外部因素。因此,在某些情况下,CLR 会插入 Win32 调用,而在其他情况下则不会。
  • 行为也会随着不同的 CLR 版本而改变

有一个特定的组件 - 垃圾收集器 (GC) - 如果存在内存压力,它会中断 .net 线程并对该线程进行一些处理(请参阅What happens during a garbage collection)。现在,如果 GC 执行失败的 Win32 调用,这将中断您对 GetLastError 的调用。

总而言之,您有许多未知因素会影响GetLastError 的可靠性。在开发/测试时您很可能不会发现不可靠性问题,但它可能随时在生产中爆炸。所以一定要使用[DllImport(SetLastError = true)]Marshal.GetLastWin32Error() 来提高你的睡眠质量;-)

【讨论】:

    【解决方案3】:

    在 [DllImport("kernel32.dll", SetLastError = true)] 中,SetLastError 属性是否使框架存储错误代码以供 Marshal.GetLastWin32Error() 使用?

    是的,如 DllImportAttribute.SetLastError Field 中所述

    是否有普通 GetLastError 无法给出正确结果的示例?

    Marshal.GetLastWin32Error Method 中所述,如果框架本身(例如垃圾收集器)调用任何在您对本地方法的调用和GetLastError 之间设置错误值的本地方法,您将获得框架调用的错误值而不是你的电话。

    我真的必须使用 Marshal.GetLastWin32Error() 吗?

    由于您无法确保框架永远不会在您的调用和对GetLastError 的调用之间调用本机方法,所以是的。还有,为什么不呢?

    这个“问题”框架版本是否相关?

    绝对可以(例如垃圾收集器中的更改),但不是必须的。

    【讨论】:

    • 我检查了 GC,但还没有发现任何问题。请参阅我的示例代码中的编辑。
    • @mini-me GC 只是一个例子,我不知道它是否真的会导致问题。但是如果并发 GC 被禁用,它肯定可以在你的线程中运行。不要只考虑框架中的自动和/或后台操作。也许您有一次会调用一些(托管)框架 API,该 API 在您对本机 API 和 GetLastError 的调用之间内部调用本机 API。你真的要在调用GetLastError之前分析你所做的所有调用的实现吗?
    • 不是,但是调用WinApi函数后的第一件事就是调用GetLastError,不要调用另一个WinApi函数,也不要玩俄罗斯方块。此外Marshal.GetLastWin32Error 只是不同层上的相同值。如果 CLR 设计不当并调用其他 WInApi 函数,谁能保证它不会被另一个 SetLastError = true 标记导入调用覆盖此值?
    • CLR 的设计目标是在两个 P/Invoke 调用之间不调用其他 Windows API 函数。因此,如果它在任何情况下都这样做,这并不表示设计不好。在许多情况下,支持托管代码所需的额外处理可能会导致两个语句之间的额外处理。这设计的,它确实不是一个缺陷,但实际上允许您的代码以您期望的方式工作。 Marshal.GetLastWin32Error 是专门添加的,以便仍然可以使用 API。
    • 请注意,.NET 保证所有隐藏处理都不会覆盖Marshal.GetLastWin32Error 的隐藏值。因此,如果您使用SetLastError = true 调用 P/Invoke 函数,并且在同一线程上没有执行其他 P/Invoke 调用(这也意味着不调用本身可能 P/Invoke 的库函数),那么 @987654332 @ 将返回 GetLastError 的值,就像在原始 P/Invoke 调用返回时一样。托管世界可以控制Marshal.GetLastWin32Error 何时更改,但不能控制GetLastError API 函数何时更改。
    猜你喜欢
    • 1970-01-01
    • 2019-07-31
    • 2017-12-29
    • 2016-07-29
    • 2017-12-21
    • 2021-08-09
    • 2013-02-11
    • 2020-10-18
    • 2010-09-30
    相关资源
    最近更新 更多