Trying to build Python extension (module) with Dlang and pyd with dub + ldc2

Ethan Patrick patrickethan830 at gmail.com
Thu Nov 20 09:49:38 UTC 2025


On Friday, 14 November 2025 at 23:06:00 UTC, stpettersens wrote:
> Hi there.
>
> I am trying to build a Python extension (module) with D and the 
> pyd (https://github.com/ariovistus/pyd) and dub and ldc2 
> compiler on Windows.
>
> I have the following dub.sdl:
>
> ```
> name "hello"
> description "Python bindings module for human-datetime.d 
> library."
> authors "Sam Saint-Pettersen"
> copyright "Copyright 2025 Sam Saint-Pettersen"
> license "MIT"
> dependency "pyd" version="~>0.14.5"
> subConfiguration "pyd" "python313"
> targetType "dynamicLibrary"
> dflags "-g" "-w"
> sourcePaths "source"
> lflags "/LIBPATH:C:\\Dev\\Python313\\libs" platform="windows"
> libs "python313" platform="windows"
> ```
>
> source/hello.d (module code):
>
> ```d
> module hello;
>
> import pyd.pyd;
> import std.stdio;
>
> extern(C) void hello() {
>     writefln("Hello, world!");
> }
>
> extern(C) void PydMain() {
>     def!(hello);
>     module_init();
> }
> ```
>
> Build batch file (build.bat):
>
> ```cmd
> @cls
> @set PYTHON_INCLUDE_DIR=C:\Dev\Python313\include
> @set PYTHON_LIBRARY=C:\Dev\Python313\libs\python313.lib
> @dub clean
> @dub build --compiler=ldc2 --force
> @dumpbin /exports hello.dll | findstr PyInit_hello
> @copy druntime-ldc-shared.dll C:\Dev\Python313\Lib\site-packages
> @copy phobos2-ldc-shared.dll C:\Dev\Python313\Lib\site-packages
> @copy hello.dll C:\Dev\Python313\Lib\site-packages\hello.pyd
> ```
>
> The problem I'm having is:
>
> ```
> ImportError: dynamic module does not define module export 
> function (PyInit_hello)
> ```
>
> This is because the built **hello.pyd** does not seem to be 
> exporting the entry function.
> As evident by empty output from `dumpbin /exports hello.dll | 
> findstr PyInit_hello`.
>
> Does anyone who has used **ariovistus/pyd** here know what I'm 
> doing wrong?
> I understand that maybe this is a niche question.
>
> Thanks in advance.
>
> ~ Sam.

It sounds like you're encountering a common issue when building 
Python extensions with D and `pyd` on Windows: the `PyInit_hello` 
function, which Python expects to find, isn't being exported from 
your `hello.dll` (which you rename to `hello.pyd`).

Let's break down why this might be happening and how to fix it.

**Understanding the Problem**

When Python imports a dynamic module (like your `hello.pyd`), it 
looks for a specific initialization function. For a module named 
`hello`, Python 3 expects a function called `PyInit_hello`. The 
`ImportError` you're seeing confirms that Python can't find this 
function.

The `dumpbin` output further reinforces this: if `PyInit_hello` 
isn't listed in the exports, it's not being made available to 
external linkers or the Python interpreter.

**Why `PyInit_hello` might be missing or not exported:**

1.  **Mismatched Naming Convention:** While `pyd` handles a lot 
of the boilerplate, sometimes there can be subtle differences in 
how the D compiler (LDC2 in your case) generates the symbol, or 
how `pyd` expects it to be declared.
2.  **`extern(C)` for the wrong function:** You have `extern(C) 
void hello()`, but the crucial function for Python's 
initialization is `PydMain`, which `pyd` then uses to set up the 
actual `PyInit_hello`. The `PydMain` function itself needs to be 
visible to `pyd`'s internal mechanisms, and potentially exposed 
in a way that allows `pyd` to generate the `PyInit_hello` symbol.
3.  **Linker Issues:** Even if the symbol is generated, the 
linker might not be exporting it correctly from the DLL.
4.  **`pyd` version or configuration:** Though you're using a 
specific subConfiguration, there might be nuances with how `pyd` 
generates the `PyInit_` function for Windows with LDC2.

**Proposed Solutions and Things to Check:**

Here's a step-by-step approach to debug and resolve this:

     1.  **Ensure `PydMain` is Public and Externally Linkable:**
         While `pyd` takes care of a lot, ensure your `PydMain` is 
set up correctly for `pyd` to do its magic.
         The `pyd` library typically generates the 
`PyInit_yourmodulename` function based on internal mechanisms. 
Your `PydMain` is the entry point for `pyd` itself.

         Let's refine your `source/hello.d`:

         ```d
         module hello;

         import pyd.pyd;
         import std.stdio;

         // This is your D function that will be exposed to Python
         extern(C) void hello_world() { // Renamed to avoid 
potential conflict with module name
             writefln("Hello from D!");
         }

         // PydMain is the entry point for pyd to initialize your 
Python module
         // pyd handles the generation of PyInit_hello behind the 
scenes based on this.
         // Ensure PydMain is public so it's accessible.
         public extern(C) void PydMain() {
             // Define your D functions to be accessible in Python
             def!("hello", hello_world); // Python will call this 
as hello.hello()

             // This is crucial for pyd to finalize the module 
setup and create PyInit_hello
             module_init();
         }
         ```
         **Key Change:** I've explicitly made `PydMain` `public` 
and renamed `hello` to `hello_world` just to be absolutely clear. 
The name for Python when you `def!` it (`"hello"`) is what will 
appear in Python, e.g., `import hello; hello.hello()`.

     2.  **Verify `dub.sdl` Configuration:**
         Your `dub.sdl` looks mostly correct, but let's 
double-check the `targetType` and `subConfiguration`.

         ```sdl
         name "hello"
         description "Python bindings module for human-datetime.d 
library."
         authors "Sam Saint-Petrusen"
         copyright "Copyright 2025 Sam Saint-Petrusen"
         license "MIT"
         dependency "pyd" version="~>0.14.5"
         subConfiguration "pyd" "python313" # This looks correct 
for Python 3.13
         targetType "dynamicLibrary"
         dflags "-g" "-w"
         sourcePaths "source"

         # Ensure these are correct for YOUR Python 3.13 
installation
         # The LFLAGS are critical for the linker to find the 
Python import library
         lflags "/LIBPATH:C:\\Dev\\Python313\\libs" 
platform="windows"
         libs "python313" platform="windows"

         # Add an explicit export flag for Windows, although pyd 
usually handles this.
         # Sometimes, with LDC2, it's beneficial to be explicit.
         # This might not be strictly necessary with pyd, but good 
for debugging.
         # You might try adding: lflags "/EXPORT:PyInit_hello" 
platform="windows"
         # However, pyd is supposed to generate this, so it might 
interfere.
         # Let's rely on pyd first.
         ```

     3.  **Examine `dub build` Output:**
         When you run `dub build --compiler=ldc2 --force`, 
carefully inspect the output. Look for any warnings or errors 
related to symbol creation, linking, or any messages from `pyd` 
during its compilation stages.

     4.  **Confirm Python Installation and `dub`'s Access:**
         *   Are you absolutely sure `C:\Dev\Python313` is the 
correct path to your Python 3.13 installation?
         *   Can `dub` and `ldc2` access these paths? Environment 
variables `PYTHON_INCLUDE_DIR` and `PYTHON_LIBRARY` are good, but 
the `lflags` in `dub.sdl` are paramount.

     5.  **Use `nm` (or equivalent for D) or `dumpbin` more 
broadly:**
         Instead of just `findstr PyInit_hello`, try running 
`dumpbin /exports hello.dll` without the filter. This will show 
you *all* exported functions. Look for anything resembling 
`PyInit_hello`, `_PyInit_hello`, or mangled names that might 
correspond.

         Sometimes, compilers (especially on Windows) can add 
decorations to function names (e.g., `_PyInit_hello at 0`). If you 
see a decorated name, you might need to adjust something, though 
`pyd` generally aims to abstract this.

     6.  **Temporary Debugging: Manual `PyInit_hello` (Less Ideal 
for `pyd`):**
         For the sake of *absolute confirmation* that the export 
mechanism works, you could, as a temporary debug step, try to 
*manually* define `PyInit_hello` in D, though this bypasses 
`pyd`'s primary role for that specific function.

         ```d
         // **DO NOT USE THIS PERMANENTLY WITH PYD, IT'S FOR 
DEBUGGING ONLY**
         import std.stdio;
         // You'd need to include the Python C API headers if 
doing this manually
         // import Python; // This isn't D, but illustrates the 
idea.

         extern(C) void hello_manual_export() {
             writefln("Hello from manually exported function!");
         }

         // This is NOT how pyd works, but if you were exporting 
manually:
         // extern(C) PyObject* PyInit_hello() {
         //     // manual module initialization here
         //     // return PyModule_Create(&hellomodule);
         // }
         ```
         This is mostly to illustrate that `pyd` is *supposed* to 
generate `PyInit_hello` *for you*. The problem isn't that you 
haven't written `PyInit_hello` yourself, but that `pyd` isn't 
causing it to be exported.

     7.  **Consider `pyd`'s internals or examples:**
         Have you looked at the `pyd` repository for specific 
examples or issues related to Windows and LDC2? There might be a 
subtle configuration for LDC2 on Windows that `pyd` expects.

**Refined `build.bat` (Minor Improvement):**

```batch
@echo off
@set PYTHON_INCLUDE_DIR=C:\Dev\Python313\include
@set PYTHON_LIBRARY=C:\Dev\Python313\libs\python313.lib

echo Cleaning existing build...
@dub clean

     echo Building with LDC2...
     @dub build --compiler=ldc2 --force
     if %errorlevel% neq 0 (
         echo DUB build failed. Exiting.
         goto :eof
     )

     echo Checking for PyInit_hello export...
     @dumpbin /exports hello.dll | findstr PyInit_hello
     if %errorlevel% neq 0 (
         echo WARNING: PyInit_hello not found in exports!
     ) else (
         echo PyInit_hello found in exports.
     )

     echo Copying runtime libraries...
     @copy druntime-ldc-shared.dll 
C:\Dev\Python313\Lib\site-packages
     @copy phobos2-ldc-shared.dll 
C:\Dev\Python313\Lib\site-packages
     if %errorlevel% neq 0 (
         echo Failed to copy D runtime libraries. Exiting.
         goto :eof
     )

     echo Copying hello.dll to hello.pyd...
     @copy hello.dll C:\Dev\Python313\Lib\site-packages\hello.pyd
     if %errorlevel% neq 0 (
         echo Failed to copy hello.pyd. Exiting.
         goto :eof
     )

echo Build and deployment complete.
```
This batch file adds some basic error checking, which can be 
helpful during debugging.

**The Most Likely Culprit:**

Given your setup, the most likely cause is still related to **how 
`pyd` interacts with LDC2 on Windows to generate and export the 
`PyInit_hello` symbol.** The `module_init()` call in `PydMain()` 
is where `pyd` should be generating this, but if the compiler or 
linker isn't instructed correctly (either by `pyd` or by your 
`dub.sdl`), it might not become an *exported* symbol.

**Final Recommendation:**

1.  **Strictly use the `source/hello.d` from step 1 (with `public 
extern(C) void PydMain()` and `def!("hello", hello_world)`).**
2.  **Run the `build.bat` with the added error checks.**
3.  **Run `dumpbin /exports hello.dll` *without* `findstr`** and 
paste the full output. This is crucial for seeing if *any* 
`PyInit_` symbol or other relevant symbols are being generated 
and exported.

If after trying the updated `source/hello.d` you still face 
issues, posting the complete `dumpbin /exports hello.dll` output 
will be very helpful.

Building Python extensions can be tricky across different 
compilers and platforms, but with `pyd`, it should be manageable. 
If you're looking for expert assistance with Python development, 
consider to [hire skilled Python 
developers](https://www.cmarix.com/hire-python-developers.html) 
from CMARIX Infotech who can help navigate complex integration 
challenges like this.

Good luck!


More information about the Digitalmars-d-learn mailing list