Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Doc/library/mmap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ update the underlying file.

To map anonymous memory, -1 should be passed as the fileno along with the length.

.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT, offset=0)
.. class:: mmap(fileno, length, tagname=None, \
access=ACCESS_DEFAULT, offset=0, *, trackfd=True)

**(Windows version)** Maps *length* bytes from the file specified by the
file handle *fileno*, and creates a mmap object. If *length* is larger
file descriptor *fileno*, and creates a mmap object. If *length* is larger
than the current size of the file, the file is extended to contain *length*
bytes. If *length* is ``0``, the maximum length of the map is the current
size of the file, except that if the file is empty Windows raises an
Expand All @@ -69,6 +70,17 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
will be relative to the offset from the beginning of the file. *offset*
defaults to 0. *offset* must be a multiple of the :const:`ALLOCATIONGRANULARITY`.

If *trackfd* is ``False``, the file handle corresponding to *fileno* will
not be duplicated, and the resulting :class:`!mmap` object will not
be associated with the map's underlying file.
This means that the :meth:`~mmap.mmap.size` and :meth:`~mmap.mmap.resize`
methods will fail.
This mode is useful to limit the number of open file handles.
The original file can be renamed (but not deleted) after closing *fileno*.

.. versionchanged:: next
The *trackfd* parameter was added.

.. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap

.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, \
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,15 @@ math
(Contributed by Bénédikt Tran in :gh:`135853`.)


mmap
----

* :class:`mmap.mmap` now has a *trackfd* parameter on Windows;
if it is ``False``, the file handle corresponding to *fileno* will
not be duplicated.
(Contributed by Serhiy Storchaka in :gh:`78502`.)


os.path
-------

Expand Down
79 changes: 37 additions & 42 deletions Lib/test/test_mmap.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from test import support
from test.support import (
requires, _2G, _4G, gc_collect, cpython_only, is_emscripten, is_apple,
in_systemd_nspawn_sync_suppressed,
Expand Down Expand Up @@ -270,42 +271,45 @@ def test_access_parameter(self):
self.assertRaises(TypeError, m.write_byte, 0)
m.close()

@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
def test_trackfd_parameter(self):
@support.subTests('close_original_fd', (True, False))
def test_trackfd_parameter(self, close_original_fd):
size = 64
with open(TESTFN, "wb") as f:
f.write(b"a"*size)
for close_original_fd in True, False:
with self.subTest(close_original_fd=close_original_fd):
with open(TESTFN, "r+b") as f:
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
if close_original_fd:
f.close()
self.assertEqual(len(m), size)
with self.assertRaises(OSError) as err_cm:
m.size()
self.assertEqual(err_cm.exception.errno, errno.EBADF)
with self.assertRaises(ValueError):
m.resize(size * 2)
with self.assertRaises(ValueError):
m.resize(size // 2)
self.assertEqual(m.closed, False)

# Smoke-test other API
m.write_byte(ord('X'))
m[2] = ord('Y')
m.flush()
with open(TESTFN, "rb") as f:
self.assertEqual(f.read(4), b'XaYa')
self.assertEqual(m.tell(), 1)
m.seek(0)
self.assertEqual(m.tell(), 0)
self.assertEqual(m.read_byte(), ord('X'))

self.assertEqual(m.closed, True)
self.assertEqual(os.stat(TESTFN).st_size, size)

@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
with open(TESTFN, "r+b") as f:
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
if close_original_fd:
f.close()
self.assertEqual(len(m), size)
with self.assertRaises(OSError) as err_cm:
m.size()
self.assertEqual(err_cm.exception.errno, errno.EBADF)
with self.assertRaises(ValueError):
m.resize(size * 2)
with self.assertRaises(ValueError):
m.resize(size // 2)
self.assertIs(m.closed, False)

# Smoke-test other API
m.write_byte(ord('X'))
m[2] = ord('Y')
m.flush()
with open(TESTFN, "rb") as f:
self.assertEqual(f.read(4), b'XaYa')
self.assertEqual(m.tell(), 1)
m.seek(0)
self.assertEqual(m.tell(), 0)
self.assertEqual(m.read_byte(), ord('X'))

if os.name == 'nt' and not close_original_fd:
self.assertRaises(PermissionError, os.rename, TESTFN, TESTFN+'1')
else:
os.rename(TESTFN, TESTFN+'1')
os.rename(TESTFN+'1', TESTFN)

self.assertIs(m.closed, True)
self.assertEqual(os.stat(TESTFN).st_size, size)

def test_trackfd_neg1(self):
size = 64
with mmap.mmap(-1, size, trackfd=False) as m:
Expand All @@ -317,15 +321,6 @@ def test_trackfd_neg1(self):
m[0] = ord('a')
assert m[0] == ord('a')

@unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
def test_no_trackfd_parameter_on_windows(self):
# 'trackffd' is an invalid keyword argument for this function
size = 64
with self.assertRaises(TypeError):
mmap.mmap(-1, size, trackfd=True)
with self.assertRaises(TypeError):
mmap.mmap(-1, size, trackfd=False)

def test_bad_file_desc(self):
# Try opening a bad file descriptor...
self.assertRaises(OSError, mmap.mmap, -2, 4096)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:class:`mmap.mmap` now has a *trackfd* parameter on Windows; if it is
``False``, the file handle corresponding to *fileno* will not be duplicated.
58 changes: 31 additions & 27 deletions Modules/mmapmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ typedef struct {

#ifdef UNIX
int fd;
_Bool trackfd;
#endif

PyObject *weakreflist;
access_mode access;
_Bool trackfd;
} mmap_object;

#define mmap_object_CAST(op) ((mmap_object *)(op))
Expand Down Expand Up @@ -642,13 +642,11 @@ is_resizeable(mmap_object *self)
"mmap can't resize with extant buffers exported.");
return 0;
}
#ifdef UNIX
if (!self->trackfd) {
PyErr_SetString(PyExc_ValueError,
"mmap can't resize with trackfd=False.");
return 0;
}
#endif
if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
return 1;
PyErr_Format(PyExc_TypeError,
Expand Down Expand Up @@ -725,7 +723,7 @@ mmap_size_method(PyObject *op, PyObject *Py_UNUSED(ignored))
CHECK_VALID(NULL);

#ifdef MS_WINDOWS
if (self->file_handle != INVALID_HANDLE_VALUE) {
if (self->file_handle != INVALID_HANDLE_VALUE || !self->trackfd) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't like combining an error condition check with a state check like this, it gives us a way to call GetFileSize(INVALID_HANDLE_VALUE, ...).

Maybe this should be && self->trackfd instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

They both are state checks. The behavior is consistent with Posix -- if trackfd is false, we get an OSError. We could raise a different error, but then we need to change the Posix code too. Is it fine to do this in this PR, or better open a separate PR?

Copy link
Member

@zooba zooba Sep 2, 2025

Choose a reason for hiding this comment

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

We should never deliberately pass INVALID_HANDLE_VALUE to a Win32 API, but we might in this case because of the or condition.

If trackfd is false, we should just raise an error directly, rather than relying on the OS to generate an error code for us.

Copy link
Member

Choose a reason for hiding this comment

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

If trackfd is false, we should just raise an error directly, rather than relying on the OS to generate an error code for us.

I agree with that.

DWORD low,high;
long long size;
low = GetFileSize(self->file_handle, &high);
Expand Down Expand Up @@ -1467,7 +1465,7 @@ static PyObject *
new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict);

PyDoc_STRVAR(mmap_doc,
"Windows: mmap(fileno, length[, tagname[, access[, offset]]])\n\
"Windows: mmap(fileno, length[, tagname[, access[, offset[, trackfd]]]])\n\
\n\
Maps length bytes from the file specified by the file handle fileno,\n\
and returns a mmap object. If length is larger than the current size\n\
Expand Down Expand Up @@ -1727,16 +1725,16 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
PyObject *tagname = Py_None;
DWORD dwErr = 0;
int fileno;
HANDLE fh = 0;
int access = (access_mode)ACCESS_DEFAULT;
HANDLE fh = INVALID_HANDLE_VALUE;
int access = (access_mode)ACCESS_DEFAULT, trackfd = 1;
DWORD flProtect, dwDesiredAccess;
static char *keywords[] = { "fileno", "length",
"tagname",
"access", "offset", NULL };
"access", "offset", "trackfd", NULL };

if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|OiL", keywords,
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|OiL$p", keywords,
&fileno, &map_size,
&tagname, &access, &offset)) {
&tagname, &access, &offset, &trackfd)) {
return NULL;
}

Expand Down Expand Up @@ -1803,30 +1801,36 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
m_obj->map_handle = NULL;
m_obj->tagname = NULL;
m_obj->offset = offset;
m_obj->trackfd = trackfd;

if (fh) {
/* It is necessary to duplicate the handle, so the
Python code can close it on us */
if (!DuplicateHandle(
GetCurrentProcess(), /* source process handle */
fh, /* handle to be duplicated */
GetCurrentProcess(), /* target proc handle */
(LPHANDLE)&m_obj->file_handle, /* result */
0, /* access - ignored due to options value */
FALSE, /* inherited by child processes? */
DUPLICATE_SAME_ACCESS)) { /* options */
dwErr = GetLastError();
Py_DECREF(m_obj);
PyErr_SetFromWindowsErr(dwErr);
return NULL;
if (fh != INVALID_HANDLE_VALUE) {
if (trackfd) {
/* It is necessary to duplicate the handle, so the
Python code can close it on us */
if (!DuplicateHandle(
GetCurrentProcess(), /* source process handle */
fh, /* handle to be duplicated */
GetCurrentProcess(), /* target proc handle */
&fh, /* result */
0, /* access - ignored due to options value */
FALSE, /* inherited by child processes? */
DUPLICATE_SAME_ACCESS)) /* options */
{
dwErr = GetLastError();
Py_DECREF(m_obj);
PyErr_SetFromWindowsErr(dwErr);
return NULL;
}
m_obj->file_handle = fh;
}
if (!map_size) {
DWORD low,high;
low = GetFileSize(fh, &high);
/* low might just happen to have the value INVALID_FILE_SIZE;
so we need to check the last error also. */
if (low == INVALID_FILE_SIZE &&
(dwErr = GetLastError()) != NO_ERROR) {
(dwErr = GetLastError()) != NO_ERROR)
{
Py_DECREF(m_obj);
return PyErr_SetFromWindowsErr(dwErr);
}
Expand Down Expand Up @@ -1888,7 +1892,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
off_lo = (DWORD)(offset & 0xFFFFFFFF);
/* For files, it would be sufficient to pass 0 as size.
For anonymous maps, we have to pass the size explicitly. */
m_obj->map_handle = CreateFileMappingW(m_obj->file_handle,
m_obj->map_handle = CreateFileMappingW(fh,
NULL,
flProtect,
size_hi,
Expand Down
Loading