WinApi - GetLastError与Marshal.GetLastWin32Error

Bit*_*lue 45 c# winapi unmanaged managed marshalling

我测试了很多.但我发现那些2没有缺点!
但是看到接受的答案.


在这里读到,调用GetLastError托管代码是不安全的,因为框架可能在内部"覆盖"最后一个错误.我从来没有遇到任何明显的问题,GetLastError对我来说,.NET Framework足够智能,不会覆盖它.因此,我对该主题有几个问题:

  • in [DllImport("kernel32.dll", SetLastError = true)]SetLastError属性是否使Framework存储错误代码供使用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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


产生错误:

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());
}
Run Code Online (Sandbox Code Playgroud)
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());
}
Run Code Online (Sandbox Code Playgroud)
// 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
Run Code Online (Sandbox Code Playgroud)

我没有看到任何区别!除了Marshal.GetLastWin32Error存储来自App-> CLR-> WinApi调用的结果之外,两者的行为相同,并且GetLastError仅存储来自App-> WinApi调用的结果.


垃圾收集似乎不会调用任何WinApi函数覆盖最后一个错误代码

  • GetLastError是线程安全的.SetLastError为调用它的每个线程存储错误代码.
  • 从什么时候GC会在我的线程中运行?

Joc*_*ach 71

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

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

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

这确保了marhsallling存根在本机函数"GetLastError"之后立即调用并将其存储在本地线程中.

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

有关更多信息,请参阅GetLastError和托管代码

.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());
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

此外,这似乎取决于您正在使用的CLR!如果用.NET2编译它,它将产生"2/0"; 如果切换到.NET 4,它将输出"2/2"...

所以它取决于CLR版本,但你不应该信任本机GetLastError函数; 总是使用Marshal.GetLastWin32Error.

  • 那么,你的例子有点证明你错了.如果你交换它们,你会从`Marshal.GetLastWin32Error`获得相同的**不需要的**行为.但这实际上是错误的编程.就像你覆盖一个变量并期望它仍然具有旧值一样.根据我的理解,dllimport的东西在.NET中设计得很好,并且"GetLastError"的使用是保存的,尽管人们试图吓跑你使用它. (3认同)

Bat*_*nit 10

TL; DR

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

论证

当我读到它时,Marshal.GetLastWin32Error可以在这里找到您需要的官方解释:

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

换句话说:

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

现在@Bitterblue发现这些"插入的呼叫"并不经常发生 - 他找不到任何东西.但这并不是真的令人惊讶.为什么?因为"黑匣子测试"是否能够GetLastError可靠地工作是非常困难的:

  • 只有在插入CLR的Win32调用实际上同时失败时,才能检测到不可靠性.
  • 这些呼叫的失败可能取决于内部/外部因素.如时间/时间,内存压力,设备,计算机状态,Windows版本......
  • CLR插入Win32调用可能取决于外部因素.因此,在某些情况下,CLR会插入Win32调用,而在其他情况下则不会.
  • 行为也可以随着不同的CLR版本而改变

有一个特定的组件 - 垃圾收集器(GC) - 众所周知,如果存在内存压力并在该线程上进行一些处理(请参阅垃圾收集期间发生的情况),就会中断.net线程.现在,如果GC要执行失败的Win32呼叫,这将打破您的呼叫GetLastError.

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


cre*_*mor 5

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

是的,如DllImportAttribute.SetLastError 字段中所述

有没有一个例子,普通的 GetLastError 无法给出正确的结果?

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

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

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

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

它绝对可能是(例如垃圾收集器中的更改),但并非必须如此。

  • 请注意,.NET 保证所有隐藏处理都不会覆盖“Marshal.GetLastWin32Error”的隐藏值。因此,如果您使用 SetLastError = true 调用 P/Invoke 函数,并且在同一线程上没有执行其他 P/Invoke 调用(这也意味着不调用本身可能是 P/Invoke 的库函数),那么`Marshal.GetLastWin32Error` 将返回 `GetLastError` 的值,因为它是原始 P/Invoke 调用返回时的值。托管世界可以控制“Marshal.GetLastWin32Error”何时发生变化,但不能控制“GetLastError” API 函数何时发生变化。 (3认同)
  • 作为设计目标,CLR 并不是不在两个 P/Invoke 调用之间调用其他 Windows API 函数。因此,如果它在任何情况下这样做,并不表示设计不好。在许多情况下,支持托管代码所需的额外处理可能会导致两个语句之间在幕后进行额外的处理。这是设计使然,事实上它并不是一个缺陷,而是实际上允许您的代码按照您期望的方式工作。专门添加了“Marshal.GetLastWin32Error”,以便仍然可以使用 API。 (2认同)