pythonctypesc-stringsshell32.dlllpwstr

Calling SHGetKnownFolderPath from Python?


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:

  1. Converts the desired GUID into the cursed _GUID struct format according to Microsoft's specification
  2. Allocates result_ptr = c_char_p() which is initially a NULL pointer but will be overwritten with the pointer to the result
  3. Calls SHGetKnownFolderPath with the desired GUID struct, no flags, on the current user, passing our result_ptr by reference so its value can be overwritten
  4. If SHGetKnownFolderPath indicated success, dereferences result_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}'


Solution

  • According to [MS.Learn]: SHGetKnownFolderPath function (shlobj_core.h) (emphasis is mine):

    [out] ppszPath

    Type: PWSTR*

    When this method returns, contains the address of a pointer to a null-terminated Unicode string

    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:

    result_ptr = ctypes.c_wchar_p()  # :)
    

    Other important aspects: