Search code examples
windowsvisual-c++cpythonzig

compile CPython extension written in Zig on Windows


Zig is able to import C libraries and therefore it can be used to write a CPython extension and compile it. This is potentially really useful for me.

Here's my simple.zig Python extension

const py = @cImport({
    @cDefine("PY_SSIZE_T_CLEAN", {});
    @cInclude("Python.h");
});
const std = @import("std");
const print = std.debug.print;

const PyObject = py.PyObject;
const PyMethodDef = py.PyMethodDef;
const PyModuleDef = py.PyModuleDef;
const PyModuleDef_Base = py.PyModuleDef_Base;
const Py_BuildValue = py.Py_BuildValue;
const PyModule_Create = py.PyModule_Create;
const METH_NOARGS = py.METH_NOARGS;

fn hello(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
    _ = self;
    _ = args;
    print("welcom to ziglang\n", .{});
    return Py_BuildValue("");
}

var Methods = [_]PyMethodDef{
    PyMethodDef{
        .ml_name = "hello",
        .ml_meth = hello,
        .ml_flags = METH_NOARGS,
        .ml_doc = null,
    },
    PyMethodDef{
        .ml_name = null,
        .ml_meth = null,
        .ml_flags = 0,
        .ml_doc = null,
    },
};

var module = PyModuleDef{
    .m_base = PyModuleDef_Base{
        .ob_base = PyObject{
            .ob_refcnt = 1,
            .ob_type = null,
        },
        .m_init = null,
        .m_index = 0,
        .m_copy = null,
    },
    .m_name = "simple",
    .m_doc = null,
    .m_size = -1,
    .m_methods = &Methods,
    .m_slots = null,
    .m_traverse = null,
    .m_clear = null,
    .m_free = null,
};

pub export fn PyInit_simple() [*]PyObject {
    return PyModule_Create(&module);
}

When I compile my CPython extension written in Zig with the Zig compiler, Python is unable to import the DLL. I am using Windows 10.

zig build-lib -lc -dynamic -target x86_64-windows-msvc -I"C:\Users\me\Anaconda3\include" -L"C:\Users\me\Anaconda3\libs" -l"python39" simple.zig

When I try to import simple.dll, Python says no module found. I am using the os module to add the directory containing my extension DLL to the extension search locations, as I believe is required on Windows since Python 3.8.

import os
os.add_dll_directory(r"C:\Users\me\my_zig_project")
import simple
ModuleNotFoundError: No module named 'simple'

I suspect the problem is that CPython on Windows is compiled with the MSVC compiler (version of MSVC depends on Python version) and the Zig compiler is not compatible somehow even though I specify target environment as Windows MSVC.

Is there a way to compile a CPython extension written in Zig to run on Windows?

A minimal example would be greatly appreciated if it is possible.


Solution

  • Having seen this video https://www.youtube.com/watch?v=9q-LHP7cMfg I realized all I needed was to rename my simple.dll to simple.pyd

    Here's a working minimal example. Create simple.zig as below

    const py = @cImport({
        @cDefine("PY_SSIZE_T_CLEAN", {});
        @cInclude("Python.h");
    });
    const std = @import("std");
    const print = std.debug.print;
    
    const PyObject = py.PyObject;
    const PyMethodDef = py.PyMethodDef;
    const PyModuleDef = py.PyModuleDef;
    const PyModuleDef_Base = py.PyModuleDef_Base;
    const Py_BuildValue = py.Py_BuildValue;
    const PyModule_Create = py.PyModule_Create;
    const METH_NOARGS = py.METH_NOARGS;
    
    fn hello(self: [*c]PyObject, args: [*c]PyObject) callconv(.C) [*]PyObject {
        _ = self;
        _ = args;
        print("welcome to ziglang\n", .{});
        return Py_BuildValue("");
    }
    
    var Methods = [_]PyMethodDef{
        PyMethodDef{
            .ml_name = "hello",
            .ml_meth = hello,
            .ml_flags = METH_NOARGS,
            .ml_doc = null,
        },
        PyMethodDef{
            .ml_name = null,
            .ml_meth = null,
            .ml_flags = 0,
            .ml_doc = null,
        },
    };
    
    var module = PyModuleDef{
        .m_base = PyModuleDef_Base{
            .ob_base = PyObject{
                .ob_refcnt = 1,
                .ob_type = null,
            },
            .m_init = null,
            .m_index = 0,
            .m_copy = null,
        },
        .m_name = "simple",
        .m_doc = null,
        .m_size = -1,
        .m_methods = &Methods,
        .m_slots = null,
        .m_traverse = null,
        .m_clear = null,
        .m_free = null,
    };
    
    pub export fn PyInit_simple() [*]PyObject {
        return PyModule_Create(&module);
    }
    

    Build simple.zig to get simple.dll

    zig build-lib -lc -dynamic -I"<directory containing Python.h>" -L"<directory containing python3.lib>" -l"python3" simple.zig
    

    Rename simple.dll to simple.pyd

    Run python.exe in the directory containing simple.pyd (python.exe's version 3.X must match the python3.lib you linked against)

    >>> import simple
    >>> simple.hello()
    welcome to ziglang