从CreateFile生成的句柄构造FileStream会得到一个空流

本文关键字:一个 CreateFile 句柄 FileStream | 更新日期: 2023-09-27 18:29:44

我正试图编写一个简单的程序,该程序将采用由两列组成的CSV,并给定第一列中的关键字,返回第二列中的相应值。问题是,我需要CSV位于备用数据流中,以使程序尽可能具有可移植性(我想让它在用户将CSV文件放在可执行文件上时,CSV会被覆盖)。这就是为什么我尝试使用WinAPI的CreateFile函数——.NET不支持备用数据流。不幸的是,我失败得很惨。

在当前状态下,该程序应该读取一个名为test.csv的文件。我想对它执行CreateFile,将intPtr句柄转换为SafeFileHandle,然后将SafeFileHandler传递给FileStream构造函数。不过,我所能达到的最好成绩是一股清流。在我看来,这个程序实际上并没有得到正确的处理。当我尝试"CREATE_ALLWAYS"或"CREATE_NEW"而不是"OPEN_ALWAYS"时,无论我如何处理其余参数,都会出现"无效句柄"错误。使用"OPEN_ALWAYS",当我检查"stream.Name"的值时,我得到的是"Unknown"。

这是代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace Searcher
{
    public partial class Searcher : Form
    {
        public static List<string> keywords;
        public static List<string> values;
        public Searcher()
        {
            InitializeComponent();
            //byte[] path = Encoding.UTF8.GetBytes("C:''Users''as''Documents''test.csv");
            SafeFileHandle safeADSHandle = NativeMethods.CreateFileW("test.csv",
            NativeConstants.GENERIC_READ,
            NativeConstants.FILE_SHARE_READ,
            IntPtr.Zero,
            NativeConstants.OPEN_ALWAYS,
            0,
            IntPtr.Zero);
            //var safeADSHandle = new SafeFileHandle(handle, true);
            if (safeADSHandle.IsInvalid)
            {
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
            var stream = new FileStream(safeADSHandle, FileAccess.Read);
            MessageBox.Show(stream.Name);
            var reader = new StreamReader(stream);
            Searcher.keywords = new List<string>();
            Searcher.values = new List<string>();
            while (!reader.EndOfStream)
            {
                var line = reader.ReadLine();
                var values = line.Split(',');
                Searcher.keywords.Add(values[0]);
                Searcher.values.Add(values[1]);
            }
            cbKeyword.DataSource = Searcher.keywords;
            cbKeyword.AutoCompleteSource = AutoCompleteSource.ListItems;
        }
        private void btnSearch_Click(object sender, EventArgs e)
        {
            tbResult.Text = Searcher.values[cbKeyword.SelectedIndex];
        }
    }
}
public partial class NativeMethods
{
        [DllImportAttribute("kernel32.dll", SetLastError = true, EntryPoint = "CreateFile")]
        public static extern SafeFileHandle CreateFileW(
            [InAttribute()] [MarshalAsAttribute(UnmanagedType.LPWStr)] string lpFileName,
            uint dwDesiredAccess,
            uint dwShareMode,
            [InAttribute()] System.IntPtr lpSecurityAttributes,
            uint dwCreationDisposition,
            uint dwFlagsAndAttributes,
            [InAttribute()] System.IntPtr hTemplateFile
        );
}

public partial class NativeConstants
{
        /// GENERIC_WRITE -> (0x40000000L)
        public const int GENERIC_WRITE = 1073741824;
        public const uint GENERIC_READ = 2147483648;
        /// FILE_SHARE_DELETE -> 0x00000004
        public const int FILE_SHARE_DELETE = 4;
        /// FILE_SHARE_WRITE -> 0x00000002
        public const int FILE_SHARE_WRITE = 2;
        /// FILE_SHARE_READ -> 0x00000001
        public const int FILE_SHARE_READ = 1;
        /// OPEN_ALWAYS -> 4
        public const int OPEN_ALWAYS = 4;
        public const int CREATE_NEW = 1;
}

编辑我稍微修改了上面的代码,以反映评论后的更改。现在我没有从IntPtr转换为SafeFileHandle。也许需要提及的一件事是,在这次更改之前,我曾尝试读取handle的值。ToString(),看看它是否在改变,它是——它是一个随机数。

从CreateFile生成的句柄构造FileStream会得到一个空流

这不起作用的原因是您指定了一个没有字符集的入口点。当您没有在DllImport中指定字符集时,默认值为CharSet.Ansi,这意味着平台调用将按如下方式搜索函数:

  • 首先是未处理的导出函数,即您指定的入口点的名称CreateFile(在kernel32.dll中不存在)
  • 接下来,如果没有找到未处理的名称,它将根据字符集搜索处理后的名称,因此它将搜索CreateFileA

因此,它将找到CreateFileA导出的函数,该函数假定传递给它的任何字符串都是1字节字符ANSI格式。但是,您正在将字符串编组为宽字符串。请注意,函数的宽字符版本称为CreateFileW(ANSI和宽字符版本函数之间的这种区别在Windows API中很常见)。

要解决此问题,您只需要确保参数的编组与导入的函数所期望的相匹配。因此,您可以删除EntryPoint字段,在这种情况下,它将使用C#方法名称来查找导出的函数(因此它将查找CreateFileW)。

然而,为了使其更加清晰,我将编写您的平台调用代码如下:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateFile(
    [MarshalAs(UnmanagedType.LPTStr)] string filename,
    [MarshalAs(UnmanagedType.U4)] FileAccess access,
    [MarshalAs(UnmanagedType.U4)] FileShare share,
    IntPtr securityAttributes,
    [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
    [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
    IntPtr templateFile);

我从pinvoke.net网站上得到了这一点,它应该是你编写pinvoke代码的首选,而不是你自己动手(尤其是如果你不熟悉Windows API或编组:)。

没有错误的原因可能是CreateFile正在创建文件,因为它找不到它

不过,文件名将显示为"[未知]"。我怀疑这是因为从句柄获取文件名的代码非常简单。