Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #79187 Add MemoryMappedFile.CreateFromFile(SafeFileHandle) overload #83134

Merged
merged 18 commits into from
Mar 22, 2023

Conversation

amalabey
Copy link
Contributor

@amalabey amalabey commented Mar 8, 2023

Fixes #79187

The overload uses the handle the perform validations instead of
instantiating a FileStream and calling the FileStream overload,
because the FileStream instantiation appears to be heavy on allocations.
@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost ghost added the community-contribution Indicates that the PR has been added by a community member label Mar 8, 2023
@ghost
Copy link

ghost commented Mar 8, 2023

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #79187

Author: amalabey
Assignees: -
Labels:

area-System.IO, new-api-needs-documentation

Milestone: -

@amalabey
Copy link
Contributor Author

amalabey commented Mar 8, 2023

@dotnet-policy-service agree

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look very good to me, thank you for you contribution @amalabey!

I need two things before I hit the approve button:

  • minor improvements of the xml doc comments (please see my comment)
  • the conflict needs to be solved

Comment on lines 189 to 225
/// <summary>
/// Creates a memory-mapped file from an existing file using a SafeFileHandle,
/// and the specified access mode, name, inheritability, and capacity.
/// </summary>
/// <param name="fileHandle">The SafeFileHandle to the existing file. Caller is
/// responsible for disposing <c>fileHandle</c> when <paramref name="leaveOpen"/>==true (otherwise,
/// automatically disposed by the <c>MemoryMappedFile</c>). </param>
/// <param name="mapName">A name to assign to the memory-mapped file, or <c>null</c> for a
/// <see cref="MemoryMappedFile"/> that you do not intend to share across processes.</param>
/// <param name="capacity">The maximum size, in bytes, to allocate to the memory-mapped file.
/// Specify 0 to set the capacity to the size of <c>filestream</c>.</param>
/// <param name="access">One of the enumeration values that specifies the type of access allowed
/// to the memory-mapped file.<para>This parameter can't be set to <c>Write</c>.</para></param>
/// <param name="inheritability">One of the enumeration values that specifies whether a handle
/// to the memory-mapped file can be inherited by a child process. The default is <c>None</c>.</param>
/// <param name="leaveOpen">A value that indicates whether to close the source file handle when
/// the <see cref="MemoryMappedFile"/> is disposed.</param>
/// <returns>A memory-mapped file that has the specified characteristics.</returns>
/// <exception cref="ArgumentException">
/// <para><c>mapName</c> is <c>null</c> or an empty string.</para>
/// <para>-or-
/// <c>capacity</c> and the length of the file are zero.</para>
/// <para>-or-
/// <c>access</c> is set to Write or Write enumeration value, which is not allowed.</para>
/// <para>-or-
/// access is set to Read and <c>capacity</c> is larger than the length of <c>fileHandle</c>.</para>
/// </exception>
/// <exception cref="ArgumentNullException"><c>fileHanlde</c> is <c>null</c>.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <c>capacity</c> is less than zero.
/// <para>-or-
/// <c>capacity</c> is less than the file size.</para>
/// <para>-or-
/// <c>access</c> is not a valid <see cref="MemoryMappedFileAccess"/> enumeration value.</para>
/// <para>-or-
/// <c>inheritability</c> is not a valid <see cref="HandleInheritability"/> enumeration value.</para>
/// </exception>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The xml docs need some minor improvements:

  • instead of ==true we need to use <see langword="true" />
  • instead of <c>null</c> we need to use <see langword="null" />
  • when refering to parameters we need to use <paramref name="$name" /> syntax

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 232 to 262
if (mapName != null && mapName.Length == 0)
{
throw new ArgumentException(SR.Argument_MapNameEmptyString);
}

if (capacity < 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_PositiveOrDefaultCapacityRequired);
}

long fileSize = RandomAccess.GetLength(fileHandle);
if (capacity == 0 && fileSize == 0)
{
throw new ArgumentException(SR.Argument_EmptyFile);
}

if (access < MemoryMappedFileAccess.ReadWrite ||
access > MemoryMappedFileAccess.ReadWriteExecute)
{
throw new ArgumentOutOfRangeException(nameof(access));
}

if (access == MemoryMappedFileAccess.Write)
{
throw new ArgumentException(SR.Argument_NewMMFWriteAccessNotAllowed, nameof(access));
}

if (inheritability < HandleInheritability.None || inheritability > HandleInheritability.Inheritable)
{
throw new ArgumentOutOfRangeException(nameof(inheritability));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changes you are introduced are perfectly correct. Would you be interested in moving the argument validation logic to a separate method and reusing it in all 3 the CreateFromFile overloads (accepting path, FileStream and SafeFileHandle)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, that makes sense. I will make that refactoring and update the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

@amalabey
Copy link
Contributor Author

@adamsitnik Addressed your feedback. I had to mark a test method with ActiveIssueAttribute, because the related CreateFromFile(FileStream) test was failing due to the same reason.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks very good, PTAL at my comments. Again thank you for your contribution @amalabey !

Comment on lines 170 to 206
/// <summary>
/// Creates a memory-mapped file from an existing file using a SafeFileHandle,
/// and the specified access mode, name, inheritability, and capacity.
/// </summary>
/// <param name="fileHandle">The SafeFileHandle to the existing file. Caller is
/// responsible for disposing <paramref name="fileHandle"/> when <paramref name="leaveOpen"/> is <see langword="true" /> (otherwise,
/// automatically disposed by the <see cref="MemoryMappedFile"/>). </param>
/// <param name="mapName">A name to assign to the memory-mapped file, or <see langword="null" /> for a
/// <see cref="MemoryMappedFile"/> that you do not intend to share across processes.</param>
/// <param name="capacity">The maximum size, in bytes, to allocate to the memory-mapped file.
/// Specify 0 to set the capacity to the size of the file.</param>
/// <param name="access">One of the enumeration values that specifies the type of access allowed
/// to the memory-mapped file.<para>This parameter can't be set to <c>Write</c>.</para></param>
/// <param name="inheritability">One of the enumeration values that specifies whether a handle
/// to the memory-mapped file can be inherited by a child process. The default is <c>None</c>.</param>
/// <param name="leaveOpen">A value that indicates whether to close the source file handle when
/// the <see cref="MemoryMappedFile"/> is disposed.</param>
/// <returns>A memory-mapped file that has the specified characteristics.</returns>
/// <exception cref="ArgumentException">
/// <para><paramref name="mapName"/> is <see langword="null" /> or an empty string.</para>
/// <para>-or-
/// <paramref name="capacity"/> and the length of the file are zero.</para>
/// <para>-or-
/// <paramref name="capacity"/> is set to Write or Write enumeration value, which is not allowed.</para>
/// <para>-or-
/// access is set to Read and <paramref name="capacity"/> is larger than the length of the file.</para>
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="fileHandle"/> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="capacity"/> is less than zero.
/// <para>-or-
/// <paramref name="capacity"/> is less than the file size.</para>
/// <para>-or-
/// <paramref name="access"/> is not a valid <see cref="MemoryMappedFileAccess"/> enumeration value.</para>
/// <para>-or-
/// <paramref name="inheritability"/> is not a valid <see cref="HandleInheritability"/> enumeration value.</para>
/// </exception>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment needs some minor tweaking:

  • when referring to existing types we need to use <see cref="$typeName"/> everywhere, so IDE gives users the possibility to click the link and go to the type.
  • same goes for enum values
  • <paramref name="capacity"/> is set to Write was incorrect, I assume we need <paramref name="access"/> is set to <see cref="MemoryMappedFileAccess.Write"/>

BTW @carlossanlop @gewarren do we a doc that describes XML comments best practices?

Suggested change
/// <summary>
/// Creates a memory-mapped file from an existing file using a SafeFileHandle,
/// and the specified access mode, name, inheritability, and capacity.
/// </summary>
/// <param name="fileHandle">The SafeFileHandle to the existing file. Caller is
/// responsible for disposing <paramref name="fileHandle"/> when <paramref name="leaveOpen"/> is <see langword="true" /> (otherwise,
/// automatically disposed by the <see cref="MemoryMappedFile"/>). </param>
/// <param name="mapName">A name to assign to the memory-mapped file, or <see langword="null" /> for a
/// <see cref="MemoryMappedFile"/> that you do not intend to share across processes.</param>
/// <param name="capacity">The maximum size, in bytes, to allocate to the memory-mapped file.
/// Specify 0 to set the capacity to the size of the file.</param>
/// <param name="access">One of the enumeration values that specifies the type of access allowed
/// to the memory-mapped file.<para>This parameter can't be set to <c>Write</c>.</para></param>
/// <param name="inheritability">One of the enumeration values that specifies whether a handle
/// to the memory-mapped file can be inherited by a child process. The default is <c>None</c>.</param>
/// <param name="leaveOpen">A value that indicates whether to close the source file handle when
/// the <see cref="MemoryMappedFile"/> is disposed.</param>
/// <returns>A memory-mapped file that has the specified characteristics.</returns>
/// <exception cref="ArgumentException">
/// <para><paramref name="mapName"/> is <see langword="null" /> or an empty string.</para>
/// <para>-or-
/// <paramref name="capacity"/> and the length of the file are zero.</para>
/// <para>-or-
/// <paramref name="capacity"/> is set to Write or Write enumeration value, which is not allowed.</para>
/// <para>-or-
/// access is set to Read and <paramref name="capacity"/> is larger than the length of the file.</para>
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="fileHandle"/> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="capacity"/> is less than zero.
/// <para>-or-
/// <paramref name="capacity"/> is less than the file size.</para>
/// <para>-or-
/// <paramref name="access"/> is not a valid <see cref="MemoryMappedFileAccess"/> enumeration value.</para>
/// <para>-or-
/// <paramref name="inheritability"/> is not a valid <see cref="HandleInheritability"/> enumeration value.</para>
/// </exception>
/// <summary>
/// Creates a memory-mapped file from an existing file using a <see cref="SafeFileHandle"/>,
/// and the specified access mode, name, inheritability, and capacity.
/// </summary>
/// <param name="fileHandle">The <see cref="SafeFileHandle"/> to the existing file. Caller is
/// responsible for disposing <paramref name="fileHandle"/> when <paramref name="leaveOpen"/> is <see langword="true" /> (otherwise,
/// automatically disposed by the <see cref="MemoryMappedFile"/>). </param>
/// <param name="mapName">A name to assign to the memory-mapped file, or <see langword="null" /> for a
/// <see cref="MemoryMappedFile"/> that you do not intend to share across processes.</param>
/// <param name="capacity">The maximum size, in bytes, to allocate to the memory-mapped file.
/// Specify 0 to set the capacity to the size of the file.</param>
/// <param name="access">One of the enumeration values that specifies the type of access allowed
/// to the memory-mapped file.
/// <para>This parameter can't be set to <see cref="MemoryMappedFileAccess.Write"/></para></param>
/// <param name="inheritability">One of the enumeration values that specifies whether a handle
/// to the memory-mapped file can be inherited by a child process. The default is <see cref="HandleInheritability.None"/>.</param>
/// <param name="leaveOpen">A value that indicates whether to close the source file handle when
/// the <see cref="MemoryMappedFile"/> is disposed.</param>
/// <returns>A memory-mapped file that has the specified characteristics.</returns>
/// <exception cref="ArgumentException">
/// <para><paramref name="mapName"/> is <see langword="null" /> or an empty string.</para>
/// <para>-or-</para>
/// <para><paramref name="capacity"/> and the length of the file are zero.</para>
/// <para>-or-</para>
/// <para><paramref name="access"/> is set to <see cref="MemoryMappedFileAccess.Write"/>, which is not allowed.</para>
/// <para>-or-</para>
/// <para><paramref name="access"/> is set to <see cref="MemoryMappedFileAccess.Read"/> and <paramref name="capacity"/> is larger than the length of the file.</para>
/// </exception>
/// <exception cref="ArgumentNullException"><paramref name="fileHandle"/> is <see langword="null" />.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <para><paramref name="capacity"/> is less than zero.</para>
/// <para>-or-</para>
/// <para><paramref name="capacity"/> is less than the file size.</para>
/// <para>-or-</para>
/// <para><paramref name="access"/> is not a valid <see cref="MemoryMappedFileAccess"/> enumeration value.</para>
/// <para>-or-</para>
/// <para><paramref name="inheritability"/> is not a valid <see cref="HandleInheritability"/> enumeration value.</para>
/// </exception>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gewarren this guide is great, we should open source it!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamsitnik Just published the /// guide to the public contributor guide!
https://learn.microsoft.com/contribute/dotnet/api-documentation

[Fact]
public void InvalidArguments_SafeFileHandle()
{
// null is an invalid stream
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: minor copy-paste error

Suggested change
// null is an invalid stream
// null is an invalid handle

Comment on lines 119 to 121
using (FileStream fs = File.Open(file.Path, FileMode.Open))
{
SafeFileHandle fileHandle = fs.SafeFileHandle;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is 100% correct, but we can improve it by not creating the FileStream and then using it's SafeFileHandle, but rather using the File.OpenHandle and work with handle directly.

The reason why I am asking for this change is that when we introduce new APIs we often don't provide docs with examples for them, so some users use our tests as examples. And if they want to use the new SafeFileHandle based APIs, they should not be creating FileStream up-front. FileStream is more expensive in terms of allocations (it's simply bigger and might allocate it's own buffer), moreover accessing FileStream.SafeFileHandle is performing a sys-call, which is expensive:

internal sealed override SafeFileHandle SafeFileHandle
{
get
{
if (CanSeek)
{
// Update the file offset before exposing it since it's possible that
// in memory position is out-of-sync with the actual file position.
FileStreamHelpers.Seek(_fileHandle, _filePosition, SeekOrigin.Begin);
}

Suggested change
using (FileStream fs = File.Open(file.Path, FileMode.Open))
{
SafeFileHandle fileHandle = fs.SafeFileHandle;
using (SafeFileHandle fileHandle = File.OpenHandle(file.Path, FileMode.Open))
{

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you very much for your contribution @amalabey !

@adamsitnik adamsitnik merged commit 6bcd4aa into dotnet:main Mar 22, 2023
@amalabey amalabey deleted the issue-79187 branch March 22, 2023 22:00
@ghost ghost locked as resolved and limited conversation to collaborators Apr 22, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.IO community-contribution Indicates that the PR has been added by a community member new-api-needs-documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API Proposal]: MemoryMappedFile.CreateFromFile(SafeFileHandle fileHandle)
3 participants