Skip to content

Commit 5823ad8

Browse files
vstinnereryksun
authored andcommitted
pythongh-59705: Implement _thread.set_name() on Windows (python#128675)
Implement set_name() with SetThreadDescription() and _get_name() with GetThreadDescription(). If SetThreadDescription() or GetThreadDescription() is not available in kernelbase.dll, delete the method when the _thread module is imported. Truncate the thread name to 32766 characters. Co-authored-by: Eryk Sun <[email protected]>
1 parent 076d0eb commit 5823ad8

File tree

4 files changed

+123
-15
lines changed

4 files changed

+123
-15
lines changed

Lib/test/test_threading.py

+33-8
Original file line numberDiff line numberDiff line change
@@ -2130,6 +2130,15 @@ def test_set_name(self):
21302130

21312131
# Test long non-ASCII name (truncated)
21322132
"x" * (limit - 1) + "é€",
2133+
2134+
# Test long non-BMP names (truncated) creating surrogate pairs
2135+
# on Windows
2136+
"x" * (limit - 1) + "\U0010FFFF",
2137+
"x" * (limit - 2) + "\U0010FFFF" * 2,
2138+
"x" + "\U0001f40d" * limit,
2139+
"xx" + "\U0001f40d" * limit,
2140+
"xxx" + "\U0001f40d" * limit,
2141+
"xxxx" + "\U0001f40d" * limit,
21332142
]
21342143
if os_helper.FS_NONASCII:
21352144
tests.append(f"nonascii:{os_helper.FS_NONASCII}")
@@ -2146,15 +2155,31 @@ def work():
21462155
work_name = _thread._get_name()
21472156

21482157
for name in tests:
2149-
encoded = name.encode(encoding, "replace")
2150-
if b'\0' in encoded:
2151-
encoded = encoded.split(b'\0', 1)[0]
2152-
if truncate is not None:
2153-
encoded = encoded[:truncate]
2154-
if sys.platform.startswith("solaris"):
2155-
expected = encoded.decode("utf-8", "surrogateescape")
2158+
if not support.MS_WINDOWS:
2159+
encoded = name.encode(encoding, "replace")
2160+
if b'\0' in encoded:
2161+
encoded = encoded.split(b'\0', 1)[0]
2162+
if truncate is not None:
2163+
encoded = encoded[:truncate]
2164+
if sys.platform.startswith("solaris"):
2165+
expected = encoded.decode("utf-8", "surrogateescape")
2166+
else:
2167+
expected = os.fsdecode(encoded)
21562168
else:
2157-
expected = os.fsdecode(encoded)
2169+
size = 0
2170+
chars = []
2171+
for ch in name:
2172+
if ord(ch) > 0xFFFF:
2173+
size += 2
2174+
else:
2175+
size += 1
2176+
if size > truncate:
2177+
break
2178+
chars.append(ch)
2179+
expected = ''.join(chars)
2180+
2181+
if '\0' in expected:
2182+
expected = expected.split('\0', 1)[0]
21582183

21592184
with self.subTest(name=name, expected=expected):
21602185
work_name = None

Modules/_threadmodule.c

+81-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ get_thread_state(PyObject *module)
4747
}
4848

4949

50+
#ifdef MS_WINDOWS
51+
typedef HRESULT (WINAPI *PF_GET_THREAD_DESCRIPTION)(HANDLE, PCWSTR*);
52+
typedef HRESULT (WINAPI *PF_SET_THREAD_DESCRIPTION)(HANDLE, PCWSTR);
53+
static PF_GET_THREAD_DESCRIPTION pGetThreadDescription = NULL;
54+
static PF_SET_THREAD_DESCRIPTION pSetThreadDescription = NULL;
55+
#endif
56+
57+
5058
/*[clinic input]
5159
module _thread
5260
[clinic start generated code]*/
@@ -2368,7 +2376,7 @@ Internal only. Return a non-zero integer that uniquely identifies the main threa
23682376
of the main interpreter.");
23692377

23702378

2371-
#ifdef HAVE_PTHREAD_GETNAME_NP
2379+
#if defined(HAVE_PTHREAD_GETNAME_NP) || defined(MS_WINDOWS)
23722380
/*[clinic input]
23732381
_thread._get_name
23742382
@@ -2379,6 +2387,7 @@ static PyObject *
23792387
_thread__get_name_impl(PyObject *module)
23802388
/*[clinic end generated code: output=20026e7ee3da3dd7 input=35cec676833d04c8]*/
23812389
{
2390+
#ifndef MS_WINDOWS
23822391
// Linux and macOS are limited to respectively 16 and 64 bytes
23832392
char name[100];
23842393
pthread_t thread = pthread_self();
@@ -2393,11 +2402,26 @@ _thread__get_name_impl(PyObject *module)
23932402
#else
23942403
return PyUnicode_DecodeFSDefault(name);
23952404
#endif
2405+
#else
2406+
// Windows implementation
2407+
assert(pGetThreadDescription != NULL);
2408+
2409+
wchar_t *name;
2410+
HRESULT hr = pGetThreadDescription(GetCurrentThread(), &name);
2411+
if (FAILED(hr)) {
2412+
PyErr_SetFromWindowsErr(0);
2413+
return NULL;
2414+
}
2415+
2416+
PyObject *name_obj = PyUnicode_FromWideChar(name, -1);
2417+
LocalFree(name);
2418+
return name_obj;
2419+
#endif
23962420
}
23972421
#endif // HAVE_PTHREAD_GETNAME_NP
23982422

23992423

2400-
#ifdef HAVE_PTHREAD_SETNAME_NP
2424+
#if defined(HAVE_PTHREAD_SETNAME_NP) || defined(MS_WINDOWS)
24012425
/*[clinic input]
24022426
_thread.set_name
24032427
@@ -2410,6 +2434,7 @@ static PyObject *
24102434
_thread_set_name_impl(PyObject *module, PyObject *name_obj)
24112435
/*[clinic end generated code: output=402b0c68e0c0daed input=7e7acd98261be82f]*/
24122436
{
2437+
#ifndef MS_WINDOWS
24132438
#ifdef __sun
24142439
// Solaris always uses UTF-8
24152440
const char *encoding = "utf-8";
@@ -2455,6 +2480,35 @@ _thread_set_name_impl(PyObject *module, PyObject *name_obj)
24552480
return PyErr_SetFromErrno(PyExc_OSError);
24562481
}
24572482
Py_RETURN_NONE;
2483+
#else
2484+
// Windows implementation
2485+
assert(pSetThreadDescription != NULL);
2486+
2487+
Py_ssize_t len;
2488+
wchar_t *name = PyUnicode_AsWideCharString(name_obj, &len);
2489+
if (name == NULL) {
2490+
return NULL;
2491+
}
2492+
2493+
if (len > PYTHREAD_NAME_MAXLEN) {
2494+
// Truncate the name
2495+
Py_UCS4 ch = name[PYTHREAD_NAME_MAXLEN-1];
2496+
if (Py_UNICODE_IS_HIGH_SURROGATE(ch)) {
2497+
name[PYTHREAD_NAME_MAXLEN-1] = 0;
2498+
}
2499+
else {
2500+
name[PYTHREAD_NAME_MAXLEN] = 0;
2501+
}
2502+
}
2503+
2504+
HRESULT hr = pSetThreadDescription(GetCurrentThread(), name);
2505+
PyMem_Free(name);
2506+
if (FAILED(hr)) {
2507+
PyErr_SetFromWindowsErr((int)hr);
2508+
return NULL;
2509+
}
2510+
Py_RETURN_NONE;
2511+
#endif
24582512
}
24592513
#endif // HAVE_PTHREAD_SETNAME_NP
24602514

@@ -2598,6 +2652,31 @@ thread_module_exec(PyObject *module)
25982652
}
25992653
#endif
26002654

2655+
#ifdef MS_WINDOWS
2656+
HMODULE kernelbase = GetModuleHandleW(L"kernelbase.dll");
2657+
if (kernelbase != NULL) {
2658+
if (pGetThreadDescription == NULL) {
2659+
pGetThreadDescription = (PF_GET_THREAD_DESCRIPTION)GetProcAddress(
2660+
kernelbase, "GetThreadDescription");
2661+
}
2662+
if (pSetThreadDescription == NULL) {
2663+
pSetThreadDescription = (PF_SET_THREAD_DESCRIPTION)GetProcAddress(
2664+
kernelbase, "SetThreadDescription");
2665+
}
2666+
}
2667+
2668+
if (pGetThreadDescription == NULL) {
2669+
if (PyObject_DelAttrString(module, "_get_name") < 0) {
2670+
return -1;
2671+
}
2672+
}
2673+
if (pSetThreadDescription == NULL) {
2674+
if (PyObject_DelAttrString(module, "set_name") < 0) {
2675+
return -1;
2676+
}
2677+
}
2678+
#endif
2679+
26012680
return 0;
26022681
}
26032682

Modules/clinic/_threadmodule.c.h

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

PC/pyconfig.h.in

+4
Original file line numberDiff line numberDiff line change
@@ -753,4 +753,8 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */
753753
/* Define if libssl has X509_VERIFY_PARAM_set1_host and related function */
754754
#define HAVE_X509_VERIFY_PARAM_SET1_HOST 1
755755

756+
// Truncate the thread name to 64 characters. The OS limit is 32766 wide
757+
// characters, but long names aren't of practical use.
758+
#define PYTHREAD_NAME_MAXLEN 32766
759+
756760
#endif /* !Py_CONFIG_H */

0 commit comments

Comments
 (0)