I've written this minimal reproducible example to calculate the Desktop folder on Windows "the hard way" (using SHGetKnownFolderPath
), but I seem to end up with a Success error code while the output buffer only yields b'C'
when dereferenced via the .result
property of c_char_p
. What am I doing wrong?
My code does this:
- Converts the desired GUID into the cursed
_GUID
struct format according to Microsoft's specification - Allocates
result_ptr = c_char_p()
which is initially a NULL pointer but will be overwritten with the pointer to the result - Calls
SHGetKnownFolderPath
with the desired GUID struct, no flags, on the current user, passing ourresult_ptr
by reference so its value can be overwritten - If
SHGetKnownFolderPath
indicated success, dereferencesresult_ptr
using.value
I'm getting a result which is only a single char long, but I thought that c_char_p
is supposed to be the pointer to the start of a null-terminated string.
Is Windows writing a bogus string into my pointer, am I reading its value out wrongly, or have I made some other error in building my function?
import contextlib
import ctypes
import ctypes.wintypes
import functools
import os
import pathlib
import types
import uuid
try:
wintypes_GUID = ctypes.wintypes.GUID
except AttributeError:
class wintypes_GUID(ctypes.Structure):
# https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid
# https://github.com/enthought/comtypes/blob/1.3.1/comtypes/GUID.py
_fields_ = [
('Data1', ctypes.c_ulong),
('Data2', ctypes.c_ushort),
('Data3', ctypes.c_ushort),
('Data4', ctypes.c_ubyte * 8)
]
@classmethod
def _from_uuid(cls, u):
u = uuid.UUID(u)
u_str = f'{{{u!s}}}'
result = wintypes_GUID()
errno = ctypes.oledll.ole32.CLSIDFromString(u_str, ctypes.byref(result))
if errno == 0:
return result
else:
raise RuntimeError(f'CLSIDFromString returned error code {errno}')
DESKTOP_UUID = 'B4BFCC3A-DB2C-424C-B029-7FE99A87C641'
def get_known_folder(uuid):
# FIXME this doesn't work, seemingly returning just b'C' no matter what
result_ptr = ctypes.c_char_p()
with _freeing(ctypes.oledll.ole32.CoTaskMemFree, result_ptr):
errno = ctypes.windll.shell32.SHGetKnownFolderPath(
ctypes.pointer(wintypes_GUID._from_uuid(uuid)),
0,
None,
ctypes.byref(result_ptr)
)
if errno == 0:
result = result_ptr.value
if len(result) < 2:
import warnings
warnings.warn(f'result_ptr.value == {result!r}')
return pathlib.Path(os.fsdecode(result))
else:
raise RuntimeError(f'Shell32.SHGetKnownFolderPath returned error code {errno}')
@contextlib.contextmanager
def _freeing(freefunc, obj):
try:
yield obj
finally:
freefunc(obj)
assert get_known_folder(DESKTOP_UUID) ==\
pathlib.Path('~/Desktop').expanduser(),\
f'Result: {get_known_folder(DESKTOP_UUID)!r}; expcected: {pathlib.Path("~/Desktop").expanduser()!r}'
According to [MS.Learn]: SHGetKnownFolderPath function (shlobj_core.h) (emphasis is mine):
Function returns the path as a WIDE (016bit) string which is wchar_t*, or [Python.Docs]: class ctypes.c_wchar_p.
Check [SO]: Passing utf-16 string to a Windows function (@CristiFati's answer) for more details.
So, all you have to change (at get_known_folder very beginning) is:
Other important aspects:
Check [SO]: C function called from Python via ctypes returns incorrect value (@CristiFati's answer) for a common pitfall when working with CTypes (calling functions). That is a problem in this case
If you're not married to CTypes for resolving this issue (although it would be a good exercise), you can use [GitHub]: mhammond/pywin32 - Python for Windows (pywin32) Extensions which is a Python wrapper over WinAPIs. Documentation (WiP) can be found at [GitHub.MHammond]: Python for Win32 Extensions Help (or [ME.TimGolden]: Python for Win32 Extensions Help).
Since it consists of lots of boilerplate code, you'd have to write much less: