Repository: 0vercl0k/udmp-parser (language: python)

Issue:
Add type hints (`.pyi` file) for the python bindings
Would be really nice to have because it gives you proper autocomplete from VSCode/PyCharm out of the box. 


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index a0dcc18..46922c8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
 # udmp-parser: A Cross-Platform C++ parser library for Windows user minidumps
 
 ![Build status](https://github.com/0vercl0k/udmp-parser/workflows/Builds/badge.svg)
+[![Downloads](https://static.pepy.tech/badge/udmp-parser/month)](https://pepy.tech/project/udmp-parser)
 
 This is a cross-platform (Windows / Linux / OSX / x86 / x64) C++ library that parses Windows user [minidump](https://docs.microsoft.com/en-us/windows/win32/debug/minidump-files) dumps (`.dump /m` and **not** `.dump /f` in WinDbg usermode).
 
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3cfba4f..cbf3f80 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -9,7 +9,7 @@ project(
     udmp-parser
     DESCRIPTION "A Cross-Platform C++ parser library for Windows user minidumps."
     HOMEPAGE_URL https://github.com/0vercl0k/udmp-parser
-    VERSION 0.4
+    VERSION 0.5.0
 )
 
 set(PROJECT_AUTHOR 0vercl0k)
diff --git a/src/lib/udmp-parser.h b/src/lib/udmp-parser.h
index 21c71a8..6814911 100644
--- a/src/lib/udmp-parser.h
+++ b/src/lib/udmp-parser.h
@@ -75,9 +75,9 @@ namespace fs = std::filesystem;
 namespace udmpparser {
 
 #ifdef NDEBUG
-void DbgPrintf(const char *Format, ...) { (void)Format; }
+static void DbgPrintf(const char *Format, ...) { (void)Format; }
 #else
-void DbgPrintf(const char *Format, ...) {
+static void DbgPrintf(const char *Format, ...) {
   va_list ArgList;
   va_start(ArgList, Format);
   vfprintf(stderr, Format, ArgList);
@@ -226,7 +226,7 @@ struct Context64_t {
   uint128_t Xmm13;
   uint128_t Xmm14;
   uint128_t Xmm15;
-  uint8_t Padding[0x60];
+  std::array<uint8_t, 0x60> Padding;
   std::array<uint128_t, 26> VectorRegister;
   uint64_t VectorControl;
   uint64_t DebugControl;
diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt
index d7607bb..4f2fabf 100644
--- a/src/python/CMakeLists.txt
+++ b/src/python/CMakeLists.txt
@@ -24,7 +24,13 @@ list(APPEND CMAKE_PREFIX_PATH "${NB_DIR}")
 
 find_package(nanobind CONFIG REQUIRED)
 
-nanobind_add_module(udmp_parser NB_STATIC src/udmp_parser.cc)
+set(CMAKE_CXX_STANDARD 20)
+
+nanobind_add_module(udmp_parser NB_STATIC src/udmp_parser_utils.cc src/udmp_parser.cc)
+
+if(MSVC)
+    target_link_libraries(udmp_parser PRIVATE DbgHelp.lib)
+endif(MSVC)
 
 if(BUILD_PYTHON_PACKAGE)
     #
@@ -32,6 +38,7 @@ if(BUILD_PYTHON_PACKAGE)
     #
     target_include_directories(udmp_parser PRIVATE ../lib)
     install(TARGETS udmp_parser LIBRARY DESTINATION .)
+    install(DIRECTORY udmp_parser-stubs DESTINATION .)
 else()
     #
     # This is the general case, when built from the root cmakefile
diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml
index 8188a49..2f7a236 100644
--- a/src/python/pyproject.toml
+++ b/src/python/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
 
 [project]
 name = "udmp-parser"
-version = "0.4.3"
+version = "0.5.0"
 description = "A Cross-Platform C++ parser library for Windows user minidumps."
 readme = "README.md"
 requires-python = ">=3.8"
@@ -21,6 +21,9 @@ dependencies = []
 [project.urls]
 Homepage = "https://github.com/0vercl0k/udmp-parser"
 
+[project.scripts]
+generate_minidump = "udmp_parser.utils:generate_minidump_from_command_line"
+
 [tool.isort]
 profile = "black"
 
@@ -29,9 +32,7 @@ wheel.py-api = "cp312"
 minimum-version = "0.4"
 build-dir = "build/{wheel_tag}"
 cmake.minimum-version = "3.20"
-cmake.args = [
-    "-DBUILD_PYTHON_PACKAGE:BOOL=ON",
-]
+cmake.args = ["-DBUILD_PYTHON_PACKAGE:BOOL=ON"]
 
 [tool.cibuildwheel]
 build-verbosity = 1
diff --git a/src/python/src/udmp_parser.cc b/src/python/src/udmp_parser.cc
index b6a0ad6..90dc340 100644
--- a/src/python/src/udmp_parser.cc
+++ b/src/python/src/udmp_parser.cc
@@ -4,7 +4,7 @@
 // Released under MIT License, by 0vercl0k - 2023
 //
 // With contribution from:
-//  * hugsy - (github.com / hugsy)
+//  * hugsy - (github.com/hugsy)
 //
 
 #include "udmp-parser.h"
@@ -23,7 +23,12 @@
 
 namespace nb = nanobind;
 
+void udmp_parser_utils_module(nb::module_ &m);
+
 NB_MODULE(udmp_parser, m) {
+
+  udmp_parser_utils_module(m);
+
   nb::enum_<udmpparser::ProcessorArch_t>(m, "ProcessorArch")
       .value("X86", udmpparser::ProcessorArch_t::X86)
       .value("ARM", udmpparser::ProcessorArch_t::ARM)
@@ -193,6 +198,14 @@ NB_MODULE(udmp_parser, m) {
       .def_ro("StreamType", &udmpparser::dmp::Directory_t::StreamType)
       .def_ro("Location", &udmpparser::dmp::Directory_t::Location);
 
+  nb::class_<udmpparser::dmp::MemoryInfoListStream_t>(m, "MemoryInfoListStream")
+      .def_ro("SizeOfHeader",
+              &udmpparser::dmp::MemoryInfoListStream_t::SizeOfHeader)
+      .def_ro("SizeOfEntry",
+              &udmpparser::dmp::MemoryInfoListStream_t::SizeOfEntry)
+      .def_ro("NumberOfEntries",
+              &udmpparser::dmp::MemoryInfoListStream_t::NumberOfEntries);
+
   nb::class_<udmpparser::dmp::Memory64ListStreamHdr_t>(m,
                                                        "Memory64ListStreamHdr")
       .def_ro("NumberOfMemoryRanges",
@@ -315,7 +328,7 @@ NB_MODULE(udmp_parser, m) {
       .def("__repr__", &udmpparser::MemBlock_t::to_string);
   ;
 
-  nb::class_<udmpparser::Module_t>(m, "Modules")
+  nb::class_<udmpparser::Module_t>(m, "Module")
       .def(nb::init<const udmpparser::dmp::ModuleEntry_t &, const std::string &,
                     const void *, const void *>(),
            nb::rv_policy::take_ownership)
@@ -374,91 +387,4 @@ NB_MODULE(udmp_parser, m) {
       .def_ro_static("major", &udmpparser::Version::Major)
       .def_ro_static("minor", &udmpparser::Version::Minor)
       .def_ro_static("release", &udmpparser::Version::Release);
-
-  auto utils = m.def_submodule("utils", "Helper functions");
-  utils.def(
-      "TypeToString",
-      [](const uint32_t Type) -> std::string {
-        switch (Type) {
-        case 0x2'00'00: {
-          return "MEM_PRIVATE";
-        }
-        case 0x4'00'00: {
-          return "MEM_MAPPED";
-        }
-        case 0x1'00'00'00: {
-          return "MEM_IMAGE";
-        }
-        }
-        return "";
-      },
-      "Get a string representation of the memory type");
-
-  utils.def(
-      "StateToString",
-      [](const uint32_t State) {
-        switch (State) {
-        case 0x10'00: {
-          return "MEM_COMMIT";
-        }
-
-        case 0x20'00: {
-          return "MEM_RESERVE";
-        }
-
-        case 0x1'00'00: {
-          return "MEM_FREE";
-        }
-        }
-        return "";
-      },
-      "Get a string representation of the memory state");
-
-  utils.def(
-      "ProtectionToString",
-      [](const uint32_t Protection) {
-        struct {
-          const char *Name = nullptr;
-          uint32_t Mask = 0;
-        } Flags[] = {
-            {"PAGE_NOACCESS", 0x01},
-            {"PAGE_READONLY", 0x02},
-            {"PAGE_READWRITE", 0x04},
-            {"PAGE_WRITECOPY", 0x08},
-            {"PAGE_EXECUTE", 0x10},
-            {"PAGE_EXECUTE_READ", 0x20},
-            {"PAGE_EXECUTE_READWRITE", 0x40},
-            {"PAGE_EXECUTE_WRITECOPY", 0x80},
-            {"PAGE_GUARD", 0x100},
-            {"PAGE_NOCACHE", 0x200},
-            {"PAGE_WRITECOMBINE", 0x400},
-            {"PAGE_TARGETS_INVALID", 0x4000'0000},
-        };
-        std::stringstream ss;
-        uint32_t KnownFlags = 0;
-
-        for (const auto &Flag : Flags) {
-          if ((Protection & Flag.Mask) == 0) {
-            continue;
-          }
-
-          ss << Flag.Name << ",";
-          KnownFlags |= Flag.Mask;
-        }
-
-        const uint32_t MissingFlags = (~KnownFlags) & Protection;
-        if (MissingFlags) {
-          ss << std::hex << "0x" << MissingFlags;
-        }
-
-        std::string ProtectionString = ss.str();
-        if (ProtectionString.size() > 1 &&
-            ProtectionString[ProtectionString.size() - 1] == ',') {
-          ProtectionString =
-              ProtectionString.substr(0, ProtectionString.size() - 1);
-        }
-
-        return ProtectionString;
-      },
-      "Get a string representation of the memory protection");
 }
diff --git a/src/python/src/udmp_parser_utils.cc b/src/python/src/udmp_parser_utils.cc
new file mode 100644
index 0000000..27f2e43
--- /dev/null
+++ b/src/python/src/udmp_parser_utils.cc
@@ -0,0 +1,172 @@
+//
+// This file is part of udmp-parser project
+//
+// Released under MIT License, by 0vercl0k - 2023
+//
+// With contribution from:
+//  * hugsy - (github.com/hugsy)
+//
+
+#include "udmp-parser.h"
+
+#include <filesystem>
+#include <memory>
+#include <nanobind/nanobind.h>
+#include <nanobind/stl/filesystem.h>
+#include <nanobind/stl/string.h>
+
+namespace nb = nanobind;
+
+#ifdef _WIN32
+#include <dbghelp.h>
+#include <windows.h>
+
+bool GenerateMinidumpFromProcessId(
+    const uint32_t TargetPid, const std::filesystem::path &MiniDumpFilePath) {
+  const HANDLE File =
+      CreateFileA(MiniDumpFilePath.string().c_str(), GENERIC_WRITE, 0, nullptr,
+                  CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
+  if (File == INVALID_HANDLE_VALUE) {
+    return false;
+  }
+
+  const HANDLE Process = OpenProcess(PROCESS_ALL_ACCESS, false, TargetPid);
+  if (Process == INVALID_HANDLE_VALUE) {
+    CloseHandle(File);
+    return false;
+  }
+
+  MINIDUMP_EXCEPTION_INFORMATION ExceptionInfo = {};
+  const auto Flags = MINIDUMP_TYPE::MiniDumpWithFullMemory |
+                     MINIDUMP_TYPE::MiniDumpWithDataSegs |
+                     MINIDUMP_TYPE::MiniDumpScanMemory |
+                     MINIDUMP_TYPE::MiniDumpWithHandleData |
+                     MINIDUMP_TYPE::MiniDumpWithFullMemoryInfo;
+
+  const auto Success =
+      MiniDumpWriteDump(Process, TargetPid, File, MINIDUMP_TYPE(Flags),
+                        &ExceptionInfo, nullptr, nullptr);
+
+  CloseHandle(Process);
+  CloseHandle(File);
+  return Success;
+}
+#endif
+
+void udmp_parser_utils_module(nb::module_ &m) {
+  auto utils = m.def_submodule("utils", "Helper functions");
+
+  utils.def(
+      "TypeToString",
+      [](const uint32_t Type) -> std::string {
+        switch (Type) {
+        case 0x2'00'00: {
+          return "MEM_PRIVATE";
+        }
+        case 0x4'00'00: {
+          return "MEM_MAPPED";
+        }
+        case 0x1'00'00'00: {
+          return "MEM_IMAGE";
+        }
+        }
+        return "";
+      },
+      "Get a string representation of the memory type");
+
+  utils.def(
+      "StateToString",
+      [](const uint32_t State) {
+        switch (State) {
+        case 0x10'00: {
+          return "MEM_COMMIT";
+        }
+
+        case 0x20'00: {
+          return "MEM_RESERVE";
+        }
+
+        case 0x1'00'00: {
+          return "MEM_FREE";
+        }
+        }
+        return "";
+      },
+      "Get a string representation of the memory state");
+
+  utils.def(
+      "ProtectionToString",
+      [](const uint32_t Protection) {
+        struct {
+          const char *Name = nullptr;
+          uint32_t Mask = 0;
+        } Flags[] = {
+            {"PAGE_NOACCESS", 0x01},
+            {"PAGE_READONLY", 0x02},
+            {"PAGE_READWRITE", 0x04},
+            {"PAGE_WRITECOPY", 0x08},
+            {"PAGE_EXECUTE", 0x10},
+            {"PAGE_EXECUTE_READ", 0x20},
+            {"PAGE_EXECUTE_READWRITE", 0x40},
+            {"PAGE_EXECUTE_WRITECOPY", 0x80},
+            {"PAGE_GUARD", 0x100},
+            {"PAGE_NOCACHE", 0x200},
+            {"PAGE_WRITECOMBINE", 0x400},
+            {"PAGE_TARGETS_INVALID", 0x4000'0000},
+        };
+        std::stringstream ss;
+        uint32_t KnownFlags = 0;
+
+        for (const auto &Flag : Flags) {
+          if ((Protection & Flag.Mask) == 0) {
+            continue;
+          }
+
+          ss << Flag.Name << ",";
+          KnownFlags |= Flag.Mask;
+        }
+
+        const uint32_t MissingFlags = (~KnownFlags) & Protection;
+        if (MissingFlags) {
+          ss << std::hex << "0x" << MissingFlags;
+        }
+
+        std::string ProtectionString = ss.str();
+        if (ProtectionString.size() > 1 &&
+            ProtectionString[ProtectionString.size() - 1] == ',') {
+          ProtectionString =
+              ProtectionString.substr(0, ProtectionString.size() - 1);
+        }
+
+        return ProtectionString;
+      },
+      "Get a string representation of the memory protection");
+
+#if defined(_WIN32)
+  utils.def("generate_minidump", GenerateMinidumpFromProcessId, "TargetPid",
+            "MiniDumpFilePath",
+            "Generate a minidump for TargetPid and save it to the given path. "
+            "Returns true on success.");
+
+  utils.def(
+      "generate_minidump_from_command_line",
+      []() -> bool {
+        nb::module_ sys = nb::module_::import_("sys");
+        nb::list argv = sys.attr("argv");
+        if (!argv.is_valid()) {
+          return false;
+        }
+
+        if (argv.size() != 3) {
+          return false;
+        }
+
+        auto a1 = nb::str(nb::handle(argv[1]));
+        const auto TargetPid = uint32_t(std::atol(a1.c_str()));
+        auto a2 = nb::str(nb::handle(argv[2]));
+        return GenerateMinidumpFromProcessId(TargetPid, a2.c_str());
+      },
+      "Generate a minidump for the target TargetPid, write it to the given "
+      "path. Returns true on success.");
+#endif // _WIN32
+}
diff --git a/src/python/udmp_parser-stubs/__init__.pyi b/src/python/udmp_parser-stubs/__init__.pyi
new file mode 100644
index 0000000..a9a780a
--- /dev/null
+++ b/src/python/udmp_parser-stubs/__init__.pyi
@@ -0,0 +1,404 @@
+import os
+from typing import Union, Optional, overload
+from enum import Enum, IntEnum
+import udmp_parser
+
+
+class ProcessorArch(IntEnum):
+    X86 = 0
+    ARM = 5
+    IA64 = 6
+    AMD64 = 9
+    Unknown = 0xFFFF
+
+
+class Arch(IntEnum):
+    X86 = 0
+    X64 = 1
+
+
+kWOW64_SIZE_OF_80387_REGISTERS: int = 80
+
+
+class FloatingSaveArea32:
+    ControlWord: int
+    StatusWord: int
+    TagWord: int
+    ErrorOffset: int
+    ErrorSelector: int
+    DataOffset: int
+    DataSelector: int
+    RegisterArea: bytearray  # size =kWOW64_SIZE_OF_80387_REGISTERS
+    Cr0NpxState: int
+
+
+class Context32:
+    ContextFlags: int
+    Dr0: int
+    Dr1: int
+    Dr2: int
+    Dr3: int
+    Dr6: int
+    Dr7: int
+    FloatSave: FloatingSaveArea32
+    SegGs: int
+    SegFs: int
+    SegEs: int
+    SegDs: int
+    Edi: int
+    Esi: int
+    Ebx: int
+    Edx: int
+    Ecx: int
+    Eax: int
+    Ebp: int
+    Eip: int
+    SegCs: int
+    EFlags: int
+    Esp: int
+    SegSs: int
+    ExtendedRegisters: bytearray  # size =kWOW64_MAXIMUM_SUPPORTED_EXTENSION
+
+
+class uint128_t:
+    Low: int
+    High: int
+
+
+class Context64:
+    P1Home: int
+    P2Home: int
+    P3Home: int
+    P4Home: int
+    P5Home: int
+    P6Home: int
+    ContextFlags: int
+    MxCsr: int
+    SegCs: int
+    SegDs: int
+    SegEs: int
+    SegFs: int
+    SegGs: int
+    SegSs: int
+    EFlags: int
+    Dr0: int
+    Dr1: int
+    Dr2: int
+    Dr3: int
+    Dr6: int
+    Dr7: int
+    Rax: int
+    Rcx: int
+    Rdx: int
+    Rbx: int
+    Rsp: int
+    Rbp: int
+    Rsi: int
+    Rdi: int
+    R8: int
+    R9: int
+    R10: int
+    R11: int
+    R12: int
+    R13: int
+    R14: int
+    R15: int
+    Rip: int
+    ControlWord: int
+    StatusWord: int
+    TagWord: int
+    Reserved1: int
+    ErrorOpcode: int
+    ErrorOffset: int
+    ErrorSelector: int
+    Reserved2: int
+    DataOffset: int
+    DataSelector: int
+    Reserved3: int
+    MxCsr2: int
+    MxCsr_Mask: int
+    FloatRegisters: list[uint128_t]  # size =8
+    Xmm0: uint128_t
+    Xmm1: uint128_t
+    Xmm2: uint128_t
+    Xmm3: uint128_t
+    Xmm4: uint128_t
+    Xmm5: uint128_t
+    Xmm6: uint128_t
+    Xmm7: uint128_t
+    Xmm8: uint128_t
+    Xmm9: uint128_t
+    Xmm10: uint128_t
+    Xmm11: uint128_t
+    Xmm12: uint128_t
+    Xmm13: uint128_t
+    Xmm14: uint128_t
+    Xmm15: uint128_t
+    Padding: bytearray  # size =0x60
+    VectorRegister: list[uint128_t]  # size =26
+    VectorControl: int
+    DebugControl: int
+    LastBranchToRip: int
+    LastBranchFromRip: int
+    LastExceptionToRip: int
+    LastExceptionFromRip: int
+
+
+class Directory:
+    StreamType: StreamType = StreamType.Unused
+    LocationDescriptor32_t: int
+
+
+class FileMap:
+    def InBounds(self, arg0: int, arg1: int, /) -> bool: ...
+    def MapFile(self, arg: str, /) -> bool: ...
+    def ViewBase(self) -> int: ...
+    def __init__(self) -> None: ...
+
+
+class FixedFileInfo:
+    Signature: int = 0
+    StrucVersion: int = 0
+    FileVersionMS: int = 0
+    FileVersionLS: int = 0
+    ProductVersionMS: int = 0
+    ProductVersionLS: int = 0
+    FileFlagsMask: int = 0
+    FileFlags: int = 0
+    FileOS: int = 0
+    FileType: int = 0
+    FileSubtype: int = 0
+    FileDateMS: int = 0
+    FileDateLS: int = 0
+
+
+class Header:
+    Signature: int
+    Version: int
+    ImplementationVersion: int
+    NumberOfStreams: int
+    StreamDirectoryRva: int
+    CheckSum: int
+    Reserved: int
+    TimeDateStamp: int
+    Flags: int
+    ExpectedSignature: int
+    ValidFlagsMask: int
+
+    def LooksGood(self) -> bool: ...
+
+    def __init__(*args, **kwargs):
+        """
+        Initialize self.  See help(type(self)) for accurate signature.
+        """
+        ...
+
+
+class LocationDescriptor32:
+    DataSize: int
+    Rva: int
+
+    def __init__(*args, **kwargs):
+        """
+        Initialize self.  See help(type(self)) for accurate signature.
+        """
+        ...
+
+
+class LocationDescriptor64:
+    DataSize: int
+    Rva: int
+
+    def __init__(*args, **kwargs):
+        """
+        Initialize self.  See help(type(self)) for accurate signature.
+        """
+        ...
+
+
+class MemBlock:
+    BaseAddress: int
+    AllocationBase: int
+    AllocationProtect: int
+    RegionSize: int
+    State: int
+    Protect: int
+    Type: int
+    Data: int
+    DataSize: int
+
+    def __init__(self, arg: udmp_parser.MemoryInfo, /) -> None: ...
+    def __str__(self) -> str: ...
+
+
+class Memory64ListStreamHdr:
+    StartOfMemoryRange: int
+    DataSize: int
+
+
+class MemoryDescriptor:
+    ThreadId: int
+    SuspendCount: int
+    PriorityClass: int
+    Priority: int
+    Teb: int
+    Stack: MemoryDescriptor
+    ThreadContext: LocationDescriptor32
+
+
+class ThreadEntry:
+    ThreadId: int
+    SuspendCount: int
+    PriorityClass: int
+    Priority: int
+    Teb: int
+    Stack: MemoryDescriptor
+    ThreadContext: LocationDescriptor32
+
+
+class Thread_t:
+    ThreadId: int
+    SuspendCount: int
+    PriorityClass: int
+    Priority: int
+    Teb: int
+    Context: Union[UnknownContext, Context32, Context64]
+
+
+class MemoryDescriptor64:
+    StartOfMemoryRange: int
+    DataSize: int
+
+
+class MemoryInfoListStream:
+    SizeOfHeader: int
+    SizeOfEntry: int
+    NumberOfEntries: int
+
+
+class MemoryInfo:
+    BaseAddress: int
+    AllocationBase: int
+    AllocationProtect: int
+    __alignment1: int
+    RegionSize: int
+    State: int
+    Protect: int
+    Type: int
+    __alignment2: int
+
+
+class Module:
+    BaseAddress: int
+    AllocationBase: int
+    AllocationProtect: int
+    RegionSize: int
+    State: int
+    Protect: int
+    Type: int
+    DataSize: int
+
+    @property
+    def Data(self) -> int: ...
+    def __str__(self) -> str: ...
+
+
+class StreamType(IntEnum):
+    Unused = 0
+    ThreadList = 3
+    ModuleList = 4
+    Exception = 6
+    SystemInfo = 7
+    Memory64List = 9
+    MemoryInfoList = 16
+
+
+class SystemInfoStream:
+    ProcessorArchitecture: ProcessorArch
+    ProcessorLevel: int
+    ProcessorRevision: int
+    NumberOfProcessors: int
+    ProductType: int
+    MajorVersion: int
+    MinorVersion: int
+    BuildNumber: int
+    PlatformId: int
+    CSDVersionRva: int
+    SuiteMask: int
+    Reserved2: int
+
+
+class ExceptionRecord:
+    ExceptionCode: int
+    ExceptionFlags: int
+    ExceptionRecord: int
+    ExceptionAddress: int
+    NumberParameters: int
+    __unusedAlignment: int
+    ExceptionInformation: list[int]  # size=kEXCEPTION_MAXIMUM_PARAMETERS
+
+
+class ExceptionStream:
+    ThreadId: int
+    __alignment: int
+    ExceptionRecord: ExceptionRecord
+    ThreadContext: LocationDescriptor32
+
+
+class UnknownContext:
+    def __init__(*args, **kwargs):
+        """
+        Initialize self.  See help(type(self)) for accurate signature.
+        """
+        ...
+
+
+class UserDumpParser:
+    def ForegroundThreadId(self) -> Optional[int]: ...
+
+    def GetMemoryBlock(self, arg: int, /) -> udmp_parser.MemBlock:
+        """
+        Access a specific MemoryBlock
+        """
+        ...
+
+    def Memory(self) -> dict[int, udmp_parser.MemBlock]: ...
+
+    def Modules(self) -> dict[int, udmp_parser.Modules]:
+        """
+        Get the minidump modules
+        """
+        ...
+
+    def Parse(self, arg: os.PathLike, /) -> bool:
+        """
+        Parse the minidump given in argument.
+        """
+        ...
+
+    def ReadMemory(self, arg0: int, arg1: int, /) -> Optional[list[int]]:
+        """
+        Read bytes from memory
+        """
+        ...
+
+    def Threads(self) -> dict[int, udmp_parser.Thread]:
+        """
+        Get the minidump threads
+        """
+        ...
+
+    def __init__(self) -> None: ...
+
+
+class version:
+    def __init__(*args, **kwargs):
+        """
+        Initialize self.  See help(type(self)) for accurate signature.
+        """
+        ...
+    major: int
+
+    minor: int
+
+    release: str
diff --git a/src/python/udmp_parser-stubs/utils.pyi b/src/python/udmp_parser-stubs/utils.pyi
new file mode 100644
index 0000000..401fad4
--- /dev/null
+++ b/src/python/udmp_parser-stubs/utils.pyi
@@ -0,0 +1,36 @@
+import os
+
+
+def ProtectionToString(arg: int, /) -> str:
+    """
+    Get a string representation of the memory protection
+    """
+    ...
+
+
+def StateToString(arg: int, /) -> str:
+    """
+    Get a string representation of the memory state
+    """
+    ...
+
+
+def TypeToString(arg: int, /) -> str:
+    """
+    Get a string representation of the memory type
+    """
+    ...
+
+
+def generate_minidump(TargetPid: int, MiniDumpFilePath: os.PathLike) -> bool:
+    """
+    Generate a minidump for TargetPid and save it to the given path. Returns true on success.
+    """
+    ...
+
+
+def generate_minidump_from_command_line() -> bool:
+    """
+    Generate a minidump for the target TargetPid, write it to the given path. Returns true on success.
+    """
+    ...


Test patch:
diff --git a/src/python/tests/utils.py b/src/python/tests/utils.py
index 6ac47ed..6b19fd2 100644
--- a/src/python/tests/utils.py
+++ b/src/python/tests/utils.py
@@ -13,6 +13,8 @@
 import time
 from typing import Optional, Tuple
 
+import udmp_parser
+
 
 def get_process_id(process_name: str):
     kernel32 = ctypes.WinDLL("kernel32")
@@ -63,104 +65,6 @@ class PROCESSENTRY32(ctypes.Structure):
     return res
 
 
-def generate_minidump(process_id: int, dump_file_path: pathlib.Path) -> bool:
-    kernel32 = ctypes.WinDLL("kernel32")
-    dbghelp = ctypes.WinDLL("dbghelp")
-
-    # Constants
-    INVALID_HANDLE_VALUE = -1
-    CREATE_ALWAYS = 2
-    PROCESS_ALL_ACCESS = 0x1F0FFF
-    GENERIC_WRITE = 0x40000000
-    FILE_ATTRIBUTE_NORMAL = 0x80
-
-    MiniDumpNormal = 0x00000000
-    MiniDumpWithDataSegs = 0x00000001
-    MiniDumpWithFullMemory = 0x00000002
-    MiniDumpWithHandleData = 0x00000004
-    MiniDumpScanMemory = 0x00000010
-    MiniDumpWithFullMemoryInfo = 0x00000800
-
-    class MINIDUMP_EXCEPTION_INFORMATION(ctypes.Structure):
-        _fields_ = [
-            ("ThreadId", ctypes.c_ulong),
-            ("ExceptionPointers", ctypes.POINTER(ctypes.c_void_p)),
-            ("ClientPointers", ctypes.c_ulong),
-        ]
-
-    class MINIDUMP_CALLBACK_INFORMATION(ctypes.Structure):
-        _fields_ = [
-            ("CallbackRoutine", ctypes.c_void_p),
-            ("CallbackParam", ctypes.c_void_p),
-        ]
-
-    class MINIDUMP_USER_STREAM(ctypes.Structure):
-        _fields_ = [
-            ("Type", ctypes.c_ulong),
-            ("BufferSize", ctypes.c_ulong),
-            ("Buffer", ctypes.POINTER(ctypes.c_void_p)),
-        ]
-
-    class MINIDUMP_USER_STREAM_INFORMATION(ctypes.Structure):
-        _fields_ = [
-            ("UserStreamCount", ctypes.c_ulong),
-            ("UserStreamArray", ctypes.POINTER(MINIDUMP_USER_STREAM)),
-            ("Reserved0", ctypes.c_ulong),
-            ("Reserved1", ctypes.c_void_p),
-        ]
-
-    MiniDumpWriteDump = dbghelp.MiniDumpWriteDump
-    MiniDumpWriteDump.argtypes = [
-        ctypes.c_void_p,
-        ctypes.c_ulong,
-        ctypes.c_void_p,
-        ctypes.c_ulong,
-        ctypes.POINTER(MINIDUMP_EXCEPTION_INFORMATION),
-        ctypes.POINTER(MINIDUMP_USER_STREAM_INFORMATION),
-        ctypes.POINTER(MINIDUMP_CALLBACK_INFORMATION),
-    ]
-    MiniDumpWriteDump.restype = ctypes.c_bool
-
-    hProcess = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, process_id)
-
-    if not hProcess:
-        return False
-
-    bSuccess = False
-    hFile = kernel32.CreateFileW(
-        str(dump_file_path.absolute()),
-        GENERIC_WRITE,
-        0,
-        None,
-        CREATE_ALWAYS,
-        FILE_ATTRIBUTE_NORMAL,
-        None,
-    )
-
-    if hFile != INVALID_HANDLE_VALUE:
-        flags = (
-            MiniDumpWithFullMemory
-            | MiniDumpWithDataSegs
-            | MiniDumpScanMemory
-            | MiniDumpWithHandleData
-            | MiniDumpWithFullMemoryInfo
-        )
-        bSuccess = MiniDumpWriteDump(
-            hProcess,
-            process_id,
-            hFile,
-            flags,
-            None,
-            None,
-            None,
-        )
-
-        kernel32.CloseHandle(hFile)
-
-    kernel32.CloseHandle(hProcess)
-    return bSuccess
-
-
 def generate_minidump_from_process_name(
     process_name: str = "explorer.exe", output_dir: pathlib.Path = pathlib.Path(".")
 ) -> Optional[Tuple[int, pathlib.Path]]:
@@ -170,7 +74,7 @@ def generate_minidump_from_process_name(
 
     dump_file_path = output_dir / f"minidump-{process_name}-{int(time.time())}.dmp"
 
-    if not generate_minidump(process_id, dump_file_path):
+    if not udmp_parser.utils.generate_minidump(process_id, dump_file_path):
         return None
 
     print(f"Minidump generated successfully: PID={process_id} -> {dump_file_path}")