Skip to content

Commit 7274d07

Browse files
gh-78502: Add a trackfd parameter to mmap.mmap() on Windows (GH-138238)
If trackfd is False, the file handle corresponding to fileno will not be duplicated.
1 parent 4a33077 commit 7274d07

File tree

5 files changed

+92
-71
lines changed

5 files changed

+92
-71
lines changed

Doc/library/mmap.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ update the underlying file.
4848

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

51-
.. class:: mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT, offset=0)
51+
.. class:: mmap(fileno, length, tagname=None, \
52+
access=ACCESS_DEFAULT, offset=0, *, trackfd=True)
5253
5354
**(Windows version)** Maps *length* bytes from the file specified by the
54-
file handle *fileno*, and creates a mmap object. If *length* is larger
55+
file descriptor *fileno*, and creates a mmap object. If *length* is larger
5556
than the current size of the file, the file is extended to contain *length*
5657
bytes. If *length* is ``0``, the maximum length of the map is the current
5758
size of the file, except that if the file is empty Windows raises an
@@ -69,6 +70,17 @@ To map anonymous memory, -1 should be passed as the fileno along with the length
6970
will be relative to the offset from the beginning of the file. *offset*
7071
defaults to 0. *offset* must be a multiple of the :const:`ALLOCATIONGRANULARITY`.
7172

73+
If *trackfd* is ``False``, the file handle corresponding to *fileno* will
74+
not be duplicated, and the resulting :class:`!mmap` object will not
75+
be associated with the map's underlying file.
76+
This means that the :meth:`~mmap.mmap.size` and :meth:`~mmap.mmap.resize`
77+
methods will fail.
78+
This mode is useful to limit the number of open file handles.
79+
The original file can be renamed (but not deleted) after closing *fileno*.
80+
81+
.. versionchanged:: next
82+
The *trackfd* parameter was added.
83+
7284
.. audit-event:: mmap.__new__ fileno,length,access,offset mmap.mmap
7385

7486
.. class:: mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, \

Doc/whatsnew/3.15.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@ math
358358
(Contributed by Bénédikt Tran in :gh:`135853`.)
359359

360360

361+
mmap
362+
----
363+
364+
* :class:`mmap.mmap` now has a *trackfd* parameter on Windows;
365+
if it is ``False``, the file handle corresponding to *fileno* will
366+
not be duplicated.
367+
(Contributed by Serhiy Storchaka in :gh:`78502`.)
368+
369+
361370
os.path
362371
-------
363372

Lib/test/test_mmap.py

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from test import support
12
from test.support import (
23
requires, _2G, _4G, gc_collect, cpython_only, is_emscripten, is_apple,
34
in_systemd_nspawn_sync_suppressed,
@@ -269,41 +270,44 @@ def test_access_parameter(self):
269270
self.assertRaises(TypeError, m.write_byte, 0)
270271
m.close()
271272

272-
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
273-
def test_trackfd_parameter(self):
273+
@support.subTests('close_original_fd', (True, False))
274+
def test_trackfd_parameter(self, close_original_fd):
274275
size = 64
275276
with open(TESTFN, "wb") as f:
276277
f.write(b"a"*size)
277-
for close_original_fd in True, False:
278-
with self.subTest(close_original_fd=close_original_fd):
279-
with open(TESTFN, "r+b") as f:
280-
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
281-
if close_original_fd:
282-
f.close()
283-
self.assertEqual(len(m), size)
284-
with self.assertRaises(ValueError):
285-
m.size()
286-
with self.assertRaises(ValueError):
287-
m.resize(size * 2)
288-
with self.assertRaises(ValueError):
289-
m.resize(size // 2)
290-
self.assertEqual(m.closed, False)
291-
292-
# Smoke-test other API
293-
m.write_byte(ord('X'))
294-
m[2] = ord('Y')
295-
m.flush()
296-
with open(TESTFN, "rb") as f:
297-
self.assertEqual(f.read(4), b'XaYa')
298-
self.assertEqual(m.tell(), 1)
299-
m.seek(0)
300-
self.assertEqual(m.tell(), 0)
301-
self.assertEqual(m.read_byte(), ord('X'))
302-
303-
self.assertEqual(m.closed, True)
304-
self.assertEqual(os.stat(TESTFN).st_size, size)
305-
306-
@unittest.skipIf(os.name == 'nt', 'trackfd not present on Windows')
278+
with open(TESTFN, "r+b") as f:
279+
with mmap.mmap(f.fileno(), size, trackfd=False) as m:
280+
if close_original_fd:
281+
f.close()
282+
self.assertEqual(len(m), size)
283+
with self.assertRaises(ValueError):
284+
m.size()
285+
with self.assertRaises(ValueError):
286+
m.resize(size * 2)
287+
with self.assertRaises(ValueError):
288+
m.resize(size // 2)
289+
self.assertIs(m.closed, False)
290+
291+
# Smoke-test other API
292+
m.write_byte(ord('X'))
293+
m[2] = ord('Y')
294+
m.flush()
295+
with open(TESTFN, "rb") as f:
296+
self.assertEqual(f.read(4), b'XaYa')
297+
self.assertEqual(m.tell(), 1)
298+
m.seek(0)
299+
self.assertEqual(m.tell(), 0)
300+
self.assertEqual(m.read_byte(), ord('X'))
301+
302+
if os.name == 'nt' and not close_original_fd:
303+
self.assertRaises(PermissionError, os.rename, TESTFN, TESTFN+'1')
304+
else:
305+
os.rename(TESTFN, TESTFN+'1')
306+
os.rename(TESTFN+'1', TESTFN)
307+
308+
self.assertIs(m.closed, True)
309+
self.assertEqual(os.stat(TESTFN).st_size, size)
310+
307311
def test_trackfd_neg1(self):
308312
size = 64
309313
with mmap.mmap(-1, size, trackfd=False) as m:
@@ -315,15 +319,6 @@ def test_trackfd_neg1(self):
315319
m[0] = ord('a')
316320
assert m[0] == ord('a')
317321

318-
@unittest.skipIf(os.name != 'nt', 'trackfd only fails on Windows')
319-
def test_no_trackfd_parameter_on_windows(self):
320-
# 'trackffd' is an invalid keyword argument for this function
321-
size = 64
322-
with self.assertRaises(TypeError):
323-
mmap.mmap(-1, size, trackfd=True)
324-
with self.assertRaises(TypeError):
325-
mmap.mmap(-1, size, trackfd=False)
326-
327322
def test_bad_file_desc(self):
328323
# Try opening a bad file descriptor...
329324
self.assertRaises(OSError, mmap.mmap, -2, 4096)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:class:`mmap.mmap` now has a *trackfd* parameter on Windows; if it is
2+
``False``, the file handle corresponding to *fileno* will not be duplicated.

Modules/mmapmodule.c

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ typedef struct {
119119

120120
#ifdef UNIX
121121
int fd;
122-
_Bool trackfd;
123122
int flags;
124123
#endif
125124

126125
PyObject *weakreflist;
127126
access_mode access;
127+
_Bool trackfd;
128128
} mmap_object;
129129

130130
#define mmap_object_CAST(op) ((mmap_object *)(op))
@@ -636,13 +636,11 @@ is_resizeable(mmap_object *self)
636636
"mmap can't resize with extant buffers exported.");
637637
return 0;
638638
}
639-
#ifdef UNIX
640639
if (!self->trackfd) {
641640
PyErr_SetString(PyExc_ValueError,
642641
"mmap can't resize with trackfd=False.");
643642
return 0;
644643
}
645-
#endif
646644
if ((self->access == ACCESS_WRITE) || (self->access == ACCESS_DEFAULT))
647645
return 1;
648646
PyErr_Format(PyExc_TypeError,
@@ -734,8 +732,6 @@ mmap_size_method(PyObject *op, PyObject *Py_UNUSED(ignored))
734732
return PyLong_FromLong((long)low);
735733
size = (((long long)high)<<32) + low;
736734
return PyLong_FromLongLong(size);
737-
} else {
738-
return PyLong_FromSsize_t(self->size);
739735
}
740736
#endif /* MS_WINDOWS */
741737

@@ -750,6 +746,7 @@ mmap_size_method(PyObject *op, PyObject *Py_UNUSED(ignored))
750746
return PyLong_FromLong(status.st_size);
751747
#endif
752748
}
749+
#endif /* UNIX */
753750
else if (self->trackfd) {
754751
return PyLong_FromSsize_t(self->size);
755752
}
@@ -758,7 +755,6 @@ mmap_size_method(PyObject *op, PyObject *Py_UNUSED(ignored))
758755
"can't get size with trackfd=False");
759756
return NULL;
760757
}
761-
#endif /* UNIX */
762758
}
763759

764760
/* This assumes that you want the entire file mapped,
@@ -1476,7 +1472,7 @@ static PyObject *
14761472
new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict);
14771473

14781474
PyDoc_STRVAR(mmap_doc,
1479-
"Windows: mmap(fileno, length[, tagname[, access[, offset]]])\n\
1475+
"Windows: mmap(fileno, length[, tagname[, access[, offset[, trackfd]]]])\n\
14801476
\n\
14811477
Maps length bytes from the file specified by the file handle fileno,\n\
14821478
and returns a mmap object. If length is larger than the current size\n\
@@ -1737,16 +1733,17 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
17371733
PyObject *tagname = Py_None;
17381734
DWORD dwErr = 0;
17391735
int fileno;
1740-
HANDLE fh = 0;
1736+
HANDLE fh = INVALID_HANDLE_VALUE;
17411737
int access = (access_mode)ACCESS_DEFAULT;
1738+
int trackfd = 1;
17421739
DWORD flProtect, dwDesiredAccess;
17431740
static char *keywords[] = { "fileno", "length",
17441741
"tagname",
1745-
"access", "offset", NULL };
1742+
"access", "offset", "trackfd", NULL };
17461743

1747-
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|OiL", keywords,
1744+
if (!PyArg_ParseTupleAndKeywords(args, kwdict, "in|OiL$p", keywords,
17481745
&fileno, &map_size,
1749-
&tagname, &access, &offset)) {
1746+
&tagname, &access, &offset, &trackfd)) {
17501747
return NULL;
17511748
}
17521749

@@ -1813,30 +1810,36 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
18131810
m_obj->map_handle = NULL;
18141811
m_obj->tagname = NULL;
18151812
m_obj->offset = offset;
1813+
m_obj->trackfd = trackfd;
18161814

1817-
if (fh) {
1818-
/* It is necessary to duplicate the handle, so the
1819-
Python code can close it on us */
1820-
if (!DuplicateHandle(
1821-
GetCurrentProcess(), /* source process handle */
1822-
fh, /* handle to be duplicated */
1823-
GetCurrentProcess(), /* target proc handle */
1824-
(LPHANDLE)&m_obj->file_handle, /* result */
1825-
0, /* access - ignored due to options value */
1826-
FALSE, /* inherited by child processes? */
1827-
DUPLICATE_SAME_ACCESS)) { /* options */
1828-
dwErr = GetLastError();
1829-
Py_DECREF(m_obj);
1830-
PyErr_SetFromWindowsErr(dwErr);
1831-
return NULL;
1815+
if (fh != INVALID_HANDLE_VALUE) {
1816+
if (trackfd) {
1817+
/* It is necessary to duplicate the handle, so the
1818+
Python code can close it on us */
1819+
if (!DuplicateHandle(
1820+
GetCurrentProcess(), /* source process handle */
1821+
fh, /* handle to be duplicated */
1822+
GetCurrentProcess(), /* target proc handle */
1823+
&fh, /* result */
1824+
0, /* access - ignored due to options value */
1825+
FALSE, /* inherited by child processes? */
1826+
DUPLICATE_SAME_ACCESS)) /* options */
1827+
{
1828+
dwErr = GetLastError();
1829+
Py_DECREF(m_obj);
1830+
PyErr_SetFromWindowsErr(dwErr);
1831+
return NULL;
1832+
}
1833+
m_obj->file_handle = fh;
18321834
}
18331835
if (!map_size) {
18341836
DWORD low,high;
18351837
low = GetFileSize(fh, &high);
18361838
/* low might just happen to have the value INVALID_FILE_SIZE;
18371839
so we need to check the last error also. */
18381840
if (low == INVALID_FILE_SIZE &&
1839-
(dwErr = GetLastError()) != NO_ERROR) {
1841+
(dwErr = GetLastError()) != NO_ERROR)
1842+
{
18401843
Py_DECREF(m_obj);
18411844
return PyErr_SetFromWindowsErr(dwErr);
18421845
}
@@ -1898,7 +1901,7 @@ new_mmap_object(PyTypeObject *type, PyObject *args, PyObject *kwdict)
18981901
off_lo = (DWORD)(offset & 0xFFFFFFFF);
18991902
/* For files, it would be sufficient to pass 0 as size.
19001903
For anonymous maps, we have to pass the size explicitly. */
1901-
m_obj->map_handle = CreateFileMappingW(m_obj->file_handle,
1904+
m_obj->map_handle = CreateFileMappingW(fh,
19021905
NULL,
19031906
flProtect,
19041907
size_hi,

0 commit comments

Comments
 (0)