如何在C#中使用本机(p/invoke)重叠IO并利用async/await?

huangapple go评论127阅读模式
英文:

How do I use native (p/invoke) overlapped IO from C# utilizing async/await?

问题

我想要以一种支持异步/等待的方式在C#中使用本机交叠IO方法(通过P/Invoke)。

以下提供了如何在一般情况下使用交叠IO的良好指导:

问题:如何使用await来利用交叠IO以确定操作何时完成?

例如,如何调用CfHydratePlaceholder方法,利用交叠IO并使用async/await来确定何时完成。

英文:

I'd like to make use of native overlapped IO methods (via P/Invoke) in C# in an async/await friendly manner.

The following give good instructions on how to use overlapped IO in general:

Question: How can I make use of Overlapped IO using await to determine when the operation is complete?

For example, how can I call the method CfHydratePlaceholder utilizing overlapped IO and using async/await to determine when it is finished.

答案1

得分: -1

我使用了提到的网站信息来创建一个支持异步/await的类,用于执行Overlapped IO:

/// <summary>
/// 用于在Overlapped IO中支持异步/await的类
/// </summary>
/// <remarks>
/// 改编自http://www.beefycode.com/post/Using-Overlapped-IO-from-Managed-Code.aspx
/// 其他相关参考:
/// - https://www.codeproject.com/Articles/523355/Asynchronous-I-O-with-Thread-BindHandle
/// - https://stackoverflow.com/questions/2099947/simple-description-of-worker-and-i-o-threads-in-net
/// </remarks>
public unsafe sealed class OverlappedAsync : IDisposable
{
    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern unsafe bool CancelIoEx([In] SafeFileHandle hFile, [In] NativeOverlapped* lpOverlapped);

    // HRESULT代码997:Overlapped I/O操作正在进行中。
    // HRESULT代码995:I/O操作已因线程退出或应用程序请求而中止。
    // HRESULT代码1168:找不到元素。
    // HRESULT代码6:句柄无效。
    // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
    // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--500-999-
    // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299-
    const int ErrorIOPending = 997;
    const int ErrorOperationAborted = 995;
    const int ErrorNotFound = 1168;
    const int ErrorInvalidHandle = 6;

    readonly NativeOverlapped* _nativeOverlapped;
    readonly TaskCompletionSource<bool> _tcs = new();
    readonly SafeFileHandle _safeFileHandle;
    readonly CancellationToken _cancellationToken;

    bool _disposed = false;

    /// <summary>
    /// 表示Overlapped IO完成时的任务
    /// </summary>
    /// <exception cref="OperationCanceledException">操作已取消</exception>
    /// <exception cref="ExternalException">在Overlapped操作期间发生错误</exception>
    public Task Task => _tcs.Task;

    /// <summary>
    /// 构造一个OverlappedAsync对象,执行给定的overlappedFunc和safeHandle,用于在overlappedFunc中使用。
    /// </summary>
    /// <exception cref="OperationCanceledException">如果取消令牌被取消</exception>
    public OverlappedAsync(SafeFileHandle safeFileHandle, Func<IntPtr, int> overlappedFunc, CancellationToken ct)
    {
        if (overlappedFunc == null) throw new ArgumentNullException(nameof(overlappedFunc));
        _safeFileHandle = safeFileHandle ?? throw new ArgumentNullException(nameof(safeFileHandle));

        _safeFileHandle = safeFileHandle;
        _cancellationToken = ct;

        // 将句柄绑定到由线程池拥有的I/O完成端口
        bool success = ThreadPool.BindHandle(_safeFileHandle);
        if (!success)
        {
            throw new InvalidOperationException($"{nameof(ThreadPool.BindHandle)}调用不成功。");
        }

        // 在开始Overlapped IO操作之前,检查取消令牌是否已触发
        if (_cancellationToken.IsCancellationRequested)
        {
            _tcs.SetCanceled();
            return;
        }

        var overlapped = new Overlapped();
        _nativeOverlapped = overlapped.Pack(IOCompletionCallback, null);
        try
        {
            var nativeOverlappedIntPtr = new IntPtr(_nativeOverlapped);
            var result = overlappedFunc(nativeOverlappedIntPtr);
            ProcessOverlappedOperationResult(result);
        }
        catch
        {
            // 如果构造函数在调用overlapped.Pack后引发异常,我们需要执行Dispose操作
            // (因为调用方将没有对象可用于调用Dispose)
            Dispose();
            throw;
        }
    }

    /// <inheritdoc cref="OverlappedAsync.OverlappedAsync(SafeFileHandle, Func{IntPtr, HRESULT}, CancellationToken)"/>
    public OverlappedAsync(SafeFileHandle safeFileHandle, Func<IntPtr, int> overlappedFunc)
        : this(safeFileHandle, overlappedFunc, CancellationToken.None)
    {
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (!_disposed)
        {
            return; // 已经被释放
        }
        _disposed = true;

        if (_nativeOverlapped != null)
        {
            Overlapped.Unpack(_nativeOverlapped);
            Overlapped.Free(_nativeOverlapped);
        }
    }

    /// <summary>
    /// 当由_cancellationToken请求取消时调用。
    /// 取消IO请求
    /// </summary>
    void OnCancel()
    {
        // 如果已释放或任务已经完成,不要尝试取消
        if (_disposed || Task.IsCompleted)
        {
            return;
        }

        bool success = CancelIoEx(_safeFileHandle, _nativeOverlapped);
        if (!success)
        {
            var errorCode = Marshal.GetLastWin32Error();

            // 如果错误代码是“未找到错误”,则可能是在我们尝试取消之前,
            // IO已经完成,句柄和/或nativeOverlapped不再有效。这可以忽略。
            if (errorCode == ErrorNotFound)
            {
                return;
            }

            SetTaskExceptionCode(errorCode);
        }
    }

    /// <summary>
    /// 处理来自Overlapped操作的返回的HRESULT代码
    /// 如果IO挂起,则使用_cancellationToken注册OnCancel方法
    /// 否则,没有什么可做(因为IO已同步完成,IOCompletionCallback已被调用)
    /// </summary>
    /// <param name="resultFromOverlappedOperation"></param>
    void ProcessOverlappedOperationResult(int resultFromOverlappedOperation)
    {
        // 如果IO挂起(这是正常情况)
        if (resultFromOverlappedOperation == ErrorIOPending)
        {
            // 只在IO挂起的情况下使用_cancellationToken注册OnCancel。
            _cancellationToken.Register(OnCancel);
            return;
        }

        // 无效句柄错误不会导致回调,因此需要在此处理异常。
        if (resultFromOverlappedOperation == ErrorInvalidHandle)
        {
            Marshal.ThrowExceptionForHR(resultFromOverlappedOperation);
        }
    }

    /// <summary>
    /// 根据错误代码设置TaskCompletionSource的状态
    /// </summary>
    void SetTaskCompletionBasedOnErrorCode(uint errorCode)
    {
        if (errorCode == 0)
        {
            _tcs.SetResult(true

<details>
<summary>英文:</summary>

I used the information from the mentioned sites to create an async/await friendly class for doing Overlapped IO:

    /// &lt;summary&gt;
    /// Class to help use async/await with Overlapped class for usage with Overlapped IO
    /// &lt;/summary&gt;
    /// &lt;remarks&gt;
    /// Adapted from http://www.beefycode.com/post/Using-Overlapped-IO-from-Managed-Code.aspx
    /// Other related reference: 
    /// - https://www.codeproject.com/Articles/523355/Asynchronous-I-O-with-Thread-BindHandle
    /// - https://stackoverflow.com/questions/2099947/simple-description-of-worker-and-i-o-threads-in-net
    /// &lt;/remarks&gt;
    public unsafe sealed class OverlappedAsync : IDisposable
    {
        [DllImport(&quot;kernel32.dll&quot;, SetLastError = true, ExactSpelling = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern unsafe bool CancelIoEx([In] SafeFileHandle hFile, [In] NativeOverlapped* lpOverlapped);

        // HRESULT code 997: Overlapped I/O operation is in progress.
        // HRESULT code 995: The I/O operation has been aborted because of either a thread exit or an application request.
        // HRESULT code 1168: Element not found.
        // HRESULT code 6: The handle is invalid.
        // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
        // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--500-999-
        // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299-
        const int ErrorIOPending = 997;
        const int ErrorOperationAborted = 995;
        const int ErrorNotFound = 1168;
        const int ErrorInvalidHandle = 6;

        readonly NativeOverlapped* _nativeOverlapped;
        readonly TaskCompletionSource&lt;bool&gt; _tcs = new();
        readonly SafeFileHandle _safeFileHandle;
        readonly CancellationToken _cancellationToken;

        bool _disposed = false;

        /// &lt;summary&gt;
        /// Task representing when the overlapped IO has completed
        /// &lt;/summary&gt;
        /// &lt;exception cref=&quot;OperationCanceledException&quot;&gt;The operation was cancelled&lt;/exception&gt;
        /// &lt;exception cref=&quot;ExternalException&quot;&gt;An error occurred during the overlapped operation&lt;/exception&gt;
        public Task Task =&gt; _tcs.Task;

        /// &lt;summary&gt;
        /// Construct an OverlappedAsync and execute the given overlappedFunc and safeHandle to be used in the overlappedFunc.
        /// &lt;/summary&gt;
        /// &lt;exception cref=&quot;OperationCanceledException&quot;&gt;If the CancellationToken is cancelled&lt;/exception&gt;
        public OverlappedAsync(SafeFileHandle safeFileHandle, Func&lt;IntPtr, int&gt; overlappedFunc, CancellationToken ct)
        {
            if (overlappedFunc == null) throw new ArgumentNullException(nameof(overlappedFunc));
            _safeFileHandle = safeFileHandle ?? throw new ArgumentNullException(nameof(safeFileHandle));

            _safeFileHandle = safeFileHandle;
            _cancellationToken = ct;

            // bind the handle to an I/O Completion Port owned by the Thread Pool
            bool success = ThreadPool.BindHandle(_safeFileHandle);
            if (!success)
            {
                throw new InvalidOperationException($&quot;{nameof(ThreadPool.BindHandle)} call was unsuccessful.&quot;);
            }

            // Check if cancellation token is already triggered before beginning overlapped IO operation.
            // Check if cancellation token is already triggered before beginning overlapped IO operation.
            if (_cancellationToken.IsCancellationRequested)
            {
                _tcs.SetCanceled();
                return;
            }

            var overlapped = new Overlapped();
            _nativeOverlapped = overlapped.Pack(IOCompletionCallback, null);
            try
            {
                var nativeOverlappedIntPtr = new IntPtr(_nativeOverlapped);
                var result = overlappedFunc(nativeOverlappedIntPtr);
                ProcessOverlappedOperationResult(result);
            }
            catch
            {
                // If the constructor throws an exception after calling overlapped.Pack, we need to do the Dispose work
                // (since the caller won&#39;t have an object to call dispose on)
                Dispose();
                throw;
            }
        }

        ///&lt;inheritdoc cref=&quot;OverlappedAsync.OverlappedAsync(SafeFileHandle, Func{IntPtr, HRESULT}, CancellationToken)&quot;/&gt;
        public OverlappedAsync(SafeFileHandle safeFileHandle, Func&lt;IntPtr, int&gt; overlappedFunc)
            : this(safeFileHandle, overlappedFunc, CancellationToken.None)
        {
        }

        ///&lt;inheritdoc/&gt;
        public void Dispose()
        {
            if (!_disposed)
            {
                return; // Already disposed
            }
            _disposed = true;

            if (_nativeOverlapped != null)
            {
                Overlapped.Unpack(_nativeOverlapped);
                Overlapped.Free(_nativeOverlapped);
            }
        }

        /// &lt;summary&gt;
        ///  Called when the cancellation is requested by the _cancellationToken.  
        ///  Cancels the IO request
        /// &lt;/summary&gt;
        void OnCancel()
        {
            // If this is disposed, don&#39;t attempt cancellation.
            // If the task is already completed, then ignore the cancellation.
            if (_disposed || Task.IsCompleted)
            {
                return;
            }

            bool success = CancelIoEx(_safeFileHandle, _nativeOverlapped);
            if (!success)
            {
                var errorCode = Marshal.GetLastWin32Error();

                // If the error code is &quot;Error not Found&quot;, then it may be that by the time we tried to cancel,
                // the IO was already completed and the handle and/or the nativeOverlapped is no longer valid.  This can be ignored.
                if (errorCode == ErrorNotFound)
                {
                    return;
                }

                SetTaskExceptionCode(errorCode);
            }
        }

        /// &lt;summary&gt;
        /// Handles the HRESULT returned from the overlapped operation,
        /// If the IO is pending, register the OnCancel method with the _cancellationToken
        /// Otherwise, there is nothing to do (since the IO completed synchronously and IOCompletionCallback was already called)
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;resultFromOverlappedOperation&quot;&gt;&lt;/param&gt;
        void ProcessOverlappedOperationResult(int resultFromOverlappedOperation)
        {
            // If the IO is pending (this is the normal case)
            if (resultFromOverlappedOperation == ErrorIOPending)
            {
                // Only register the OnCancel with the _cancellationToken in the case where IO is pending.
                _cancellationToken.Register(OnCancel);
                return;
            }

            // Invalid handle error will not result in a callback, so it needs to be handled here with an exception.
            if (resultFromOverlappedOperation == ErrorInvalidHandle)
            {
                Marshal.ThrowExceptionForHR(resultFromOverlappedOperation);
            }
        }

        /// &lt;summary&gt;
        /// Set the TaskCompletionSource into the proper state based on the errorCode
        /// &lt;/summary&gt;
        void SetTaskCompletionBasedOnErrorCode(uint errorCode)
        {
            if (errorCode == 0)
            {
                _tcs.SetResult(true);
            }

            // If the error indicates that the operation was aborted and the cancellation token indicates that cancellation was requested,
            // Then set the TaskCompletionSource into the cancelled state.  This is expected to happen when cancellation is requested.
            else if (errorCode == ErrorOperationAborted &amp;&amp; _cancellationToken.IsCancellationRequested)
            {
                _tcs.SetCanceled();
            }

            // Otherwise set the TaskCompletionSource into the faulted state
            else
            {
                SetTaskExceptionCode((int)errorCode);
            }
        }

        /// &lt;summary&gt;
        /// This callback gets called in the case where the IO was overlapped.
        /// This sets the TaskCompletionSource to completed 
        /// unless there was an error (in which case the TaskCompletionSource&#39;s exception is set)
        /// &lt;/summary&gt;
        void IOCompletionCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
        {
            // It&#39;s expected that the passed in nativeOverlapped pointer always matches what we received
            // at construction (otherwise Dispose will be unpacking/freeing the wrong pointer).
            Debug.Assert(nativeOverlapped == _nativeOverlapped);

            // We don&#39;t expect the callback to be called if the TaskCompletionSource is already completed
            // (i.e. in the case where IO completed synchronously or had an error)
            Debug.Assert(!Task.IsCompleted);

            SetTaskCompletionBasedOnErrorCode(errorCode);
        }

        /// &lt;summary&gt;
        /// Set the TaskCompletion&#39;s Exception to an ExternalException with the given error code
        /// &lt;/summary&gt;
        void SetTaskExceptionCode(int code)
        {
            Debug.Assert(code &gt;= 0);
            try
            {
                // Need to throw/catch the exception so it has a valid callstack
                Marshal.ThrowExceptionForHR(code);

                // It&#39;s expected that for valid codes the above always throws, but when it encounters a code it isn&#39;t aware of
                // it does not throw.  Throw here for those cases.
                throw new Win32Exception(code);
            }
            catch (Exception ex)
            {
                // There is a race condition where both the Cancel workflow and the IOCompletionCallback flow
                // could set the Exception.  Only one of the errors will get translated into the Task&#39;s exception.
                bool success = _tcs.TrySetException(ex);
                Debug.Assert(success);
            }
        }
    }


Sample usage:

    using var overlapped = new OverlappedAsync(hFile, nativeOverlapped =&gt; CfHydratePlaceholder(hFile, 0, -1, 0, nativeOverlapped));
    await overlapped.Task;

Note: It is important the the file handle remains valid until the `OverlappedAsync.Task` has completed.

Using this approach is convenient when using native methods that do not have counterparts in .NET.  Here are some examples from the Cloud Filter API that can use this approach:

 - [CfHydratePlaceholder][1]
 - [CfConvertToPlaceholder][2]
 - [CfRevertPlaceholder][3]
 - [CfSetPinState][4]
 - [CfUpdatePlaceholder][5]


  [1]: https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfhydrateplaceholder
  [2]: https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfconverttoplaceholder
  [3]: https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfrevertplaceholder
  [4]: https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetpinstate
  [5]: https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfupdateplaceholder

</details>



huangapple
  • 本文由 发表于 2023年2月24日 05:50:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/75550677.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定