This repository was archived by the owner on Nov 26, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 24
Add a python extension example #245
Draft
cmpute
wants to merge
1
commit into
ziglang:main
Choose a base branch
from
cmpute:python-ext
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Member
|
I had to implement Diff in ziglang/translate-c
diff --git a/build/Translator.zig b/build/Translator.zig
index 76a40fc..7fd9381 100644
--- a/build/Translator.zig
+++ b/build/Translator.zig
@@ -208,6 +208,17 @@ pub fn addConfigHeader(t: *const Translator, ch: *Build.Step.ConfigHeader) void
appendIncludeArg(t.run, "-I", ch.getOutputDir());
}
+/// Links `lib` to `t.mod`, and exposes any headers installed by `lib` to translate-c.
+pub fn linkSystemLibrary(
+ t: *const Translator,
+ name: []const u8,
+ options: std.Build.Module.LinkSystemLibraryOptions,
+) !void {
+ t.mod.linkSystemLibrary(name, options);
+ const res = try t.run.step.runPkgConfig(name);
+ t.run.addArgs(res.cflags);
+}
+
/// If the value is omitted, it is set to 1.
/// `name` and `value` need not live longer than the function call.
pub fn defineCMacro(t: *const Translator, name: []const u8, value: ?[]const u8) void {
diff --git a/examples/python_extension/build.zig b/examples/python_extension/build.zig
index f4fdec0..05e7a68 100644
--- a/examples/python_extension/build.zig
+++ b/examples/python_extension/build.zig
@@ -12,13 +12,11 @@ pub fn build(b: *std.Build) !void {
// Create a step to translate Python.h directly. This also creates a Zig module from the output.
const python_h: Translator = .init(translate_c, .{
- .c_source_file = .{ .cwd_relative = "/home/jacobz/miniforge3/include/python3.12/Python.h" },
+ .c_source_file = b.path("python.h"),
.target = target,
.optimize = optimize,
});
-
- // Add Python include paths for resolving dependencies
- python_h.addIncludePath(.{ .cwd_relative = "/home/jacobz/miniforge3/include/python3.12" });
+ try python_h.linkSystemLibrary("python", .{});
// Create a module for our Python extension
const extension_module = b.createModule(.{
@@ -46,40 +44,24 @@ pub fn build(b: *std.Build) !void {
extension_lib.linker_allow_shlib_undefined = true;
// Install the shared library
- b.installArtifact(extension_lib);
+ const install_step = b.addInstallArtifact(extension_lib, .{
+ .dest_sub_path = "zig_ext.so",
+ });
+ b.getInstallStep().dependOn(&install_step.step);
// Create build step
const build_step = b.step("build", "Build the Python extension");
build_step.dependOn(&extension_lib.step);
- // Create test step
- const test_step = b.step("test", "Test the Python extension by importing and using it");
- test_step.dependOn(&extension_lib.step);
-
// Add a Python script to test the extension
- const test_python = b.addSystemCommand(&.{
- "python3", "-c",
- \\import sys;
- \\import os;
- \\# Add the zig-out/lib directory to Python path
- \\sys.path.insert(0, 'zig-out/lib');
- \\try:
- \\ import zig_ext;
- \\ print("Successfully imported Python extension 'zig_ext'!")
- \\ print("Testing zig_ext.get_greeting():");
- \\ greeting = zig_ext.get_greeting();
- \\ print(greeting)
- \\ print("Testing zig_ext.add_numbers(10, 20):");
- \\ result = zig_ext.add_numbers(10, 20);
- \\ print(f"zig_ext.add_numbers(10, 20) = {result}")
- \\ print("✅ All tests passed!")
- \\except Exception as e:
- \\ print(f"❌ Error: {e}");
- \\ import traceback;
- \\ traceback.print_exc();
- \\ sys.exit(1)
- });
+ const test_python = std.Build.Step.Run.create(b, "run test.py");
+ test_python.addArgs(&.{ "python", "test.py" });
+
+ test_python.setCwd(.{ .cwd_relative = b.build_root.path orelse "." });
+ test_python.step.dependOn(&install_step.step);
+ // Create test step
+ const test_step = b.step("test", "Test the Python extension by importing and using it");
test_step.dependOn(&test_python.step);
// Set default step to build
diff --git a/examples/python_extension/python.h b/examples/python_extension/python.h
new file mode 100644
index 0000000..576fc6d
--- /dev/null
+++ b/examples/python_extension/python.h
@@ -0,0 +1 @@
+#include <Python.h>
diff --git a/examples/python_extension/test.py b/examples/python_extension/test.py
new file mode 100644
index 0000000..c935e00
--- /dev/null
+++ b/examples/python_extension/test.py
@@ -0,0 +1,19 @@
+import sys;
+import os;
+# Add the zig-out/lib directory to Python path
+sys.path.insert(0, 'zig-out/lib')
+try:
+ import zig_ext;
+ print("Successfully imported Python extension 'zig_ext'!")
+ print("Testing zig_ext.get_greeting():")
+ greeting = zig_ext.get_greeting()
+ print(greeting)
+ print("Testing zig_ext.add_numbers(10, 20):")
+ result = zig_ext.add_numbers(10, 20)
+ print(f"zig_ext.add_numbers(10, 20) = {result}")
+ print("✅ All tests passed!")
+except Exception as e:
+ print(f"❌ Error: {e}")
+ import traceback;
+ traceback.print_exc()
+ sys.exit(1)diff in ziglang/zig
diff --git a/lib/std/Build/Step.zig b/lib/std/Build/Step.zig
index c247e69461..c67120373c 100644
--- a/lib/std/Build/Step.zig
+++ b/lib/std/Build/Step.zig
@@ -2,6 +2,7 @@ const Step = @This();
const builtin = @import("builtin");
const std = @import("../std.zig");
+const mem = std.mem;
const Io = std.Io;
const Build = std.Build;
const Allocator = std.mem.Allocator;
@@ -9,6 +10,8 @@ const assert = std.debug.assert;
const Cache = Build.Cache;
const Path = Cache.Path;
const ArrayList = std.ArrayList;
+const PkgConfigPkg = std.Build.PkgConfigPkg;
+const PkgConfigError = std.Build.PkgConfigError;
id: Id,
name: []const u8,
@@ -996,6 +999,159 @@ pub fn recursiveReset(step: *Step, gpa: Allocator) void {
}
}
+const PkgConfigResult = struct {
+ cflags: []const []const u8,
+ libs: []const []const u8,
+};
+
+/// Run pkg-config for the given library name and parse the output, returning the arguments
+/// that should be passed to zig to link the given library.
+pub fn runPkgConfig(step: *Step, lib_name: []const u8) !PkgConfigResult {
+ const wl_rpath_prefix = "-Wl,-rpath,";
+
+ const b = step.owner;
+ const pkg_name = match: {
+ // First we have to map the library name to pkg config name. Unfortunately,
+ // there are several examples where this is not straightforward:
+ // -lSDL2 -> pkg-config sdl2
+ // -lgdk-3 -> pkg-config gdk-3.0
+ // -latk-1.0 -> pkg-config atk
+ // -lpulse -> pkg-config libpulse
+ const pkgs = try getPkgConfigList(b);
+
+ // Exact match means instant winner.
+ for (pkgs) |pkg| {
+ if (mem.eql(u8, pkg.name, lib_name)) {
+ break :match pkg.name;
+ }
+ }
+
+ // Next we'll try ignoring case.
+ for (pkgs) |pkg| {
+ if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
+ break :match pkg.name;
+ }
+ }
+
+ // Prefixed "lib" or suffixed ".0".
+ for (pkgs) |pkg| {
+ if (std.ascii.indexOfIgnoreCase(pkg.name, lib_name)) |pos| {
+ const prefix = pkg.name[0..pos];
+ const suffix = pkg.name[pos + lib_name.len ..];
+ if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
+ if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
+ break :match pkg.name;
+ }
+ }
+
+ // Trimming "-1.0".
+ if (mem.endsWith(u8, lib_name, "-1.0")) {
+ const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
+ for (pkgs) |pkg| {
+ if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
+ break :match pkg.name;
+ }
+ }
+ }
+
+ return error.PackageNotFound;
+ };
+
+ var code: u8 = undefined;
+ const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
+ const stdout = if (b.runAllowFail(&[_][]const u8{
+ pkg_config_exe,
+ pkg_name,
+ "--cflags",
+ "--libs",
+ }, &code, .Ignore)) |stdout| stdout else |err| switch (err) {
+ error.ProcessTerminated => return error.PkgConfigCrashed,
+ error.ExecNotSupported => return error.PkgConfigFailed,
+ error.ExitCodeFailure => return error.PkgConfigFailed,
+ error.FileNotFound => return error.PkgConfigNotInstalled,
+ else => return err,
+ };
+
+ var zig_cflags = std.array_list.Managed([]const u8).init(b.allocator);
+ defer zig_cflags.deinit();
+ var zig_libs = std.array_list.Managed([]const u8).init(b.allocator);
+ defer zig_libs.deinit();
+
+ var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
+ while (arg_it.next()) |arg| {
+ if (mem.eql(u8, arg, "-I")) {
+ const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
+ try zig_cflags.appendSlice(&[_][]const u8{ "-I", dir });
+ } else if (mem.startsWith(u8, arg, "-I")) {
+ try zig_cflags.append(arg);
+ } else if (mem.eql(u8, arg, "-L")) {
+ const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
+ try zig_libs.appendSlice(&[_][]const u8{ "-L", dir });
+ } else if (mem.startsWith(u8, arg, "-L")) {
+ try zig_libs.append(arg);
+ } else if (mem.eql(u8, arg, "-l")) {
+ const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
+ try zig_libs.appendSlice(&[_][]const u8{ "-l", lib });
+ } else if (mem.startsWith(u8, arg, "-l")) {
+ try zig_libs.append(arg);
+ } else if (mem.eql(u8, arg, "-D")) {
+ const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
+ try zig_cflags.appendSlice(&[_][]const u8{ "-D", macro });
+ } else if (mem.startsWith(u8, arg, "-D")) {
+ try zig_cflags.append(arg);
+ } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
+ try zig_cflags.appendSlice(&[_][]const u8{ "-rpath", arg[wl_rpath_prefix.len..] });
+ } else if (b.debug_pkg_config) {
+ return step.fail("unknown pkg-config flag '{s}'", .{arg});
+ }
+ }
+
+ return .{
+ .cflags = try zig_cflags.toOwnedSlice(),
+ .libs = try zig_libs.toOwnedSlice(),
+ };
+}
+
+fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
+ if (b.pkg_config_pkg_list) |res| {
+ return res;
+ }
+ var code: u8 = undefined;
+ if (execPkgConfigList(b, &code)) |list| {
+ b.pkg_config_pkg_list = list;
+ return list;
+ } else |err| {
+ const result = switch (err) {
+ error.ProcessTerminated => error.PkgConfigCrashed,
+ error.ExecNotSupported => error.PkgConfigFailed,
+ error.ExitCodeFailure => error.PkgConfigFailed,
+ error.FileNotFound => error.PkgConfigNotInstalled,
+ error.InvalidName => error.PkgConfigNotInstalled,
+ error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
+ else => return err,
+ };
+ b.pkg_config_pkg_list = result;
+ return result;
+ }
+}
+
+fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || Build.RunError)![]const PkgConfigPkg {
+ const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
+ const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);
+ var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator);
+ errdefer list.deinit();
+ var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
+ while (line_it.next()) |line| {
+ if (mem.trim(u8, line, " \t").len == 0) continue;
+ var tok_it = mem.tokenizeAny(u8, line, " \t");
+ try list.append(PkgConfigPkg{
+ .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
+ .desc = tok_it.rest(),
+ });
+ }
+ return list.toOwnedSlice();
+}
+
test {
_ = CheckFile;
_ = CheckObject;
diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig
index 4f9900ab59..8e7a508489 100644
--- a/lib/std/Build/Step/Compile.zig
+++ b/lib/std/Build/Step/Compile.zig
@@ -9,8 +9,6 @@ const Sha256 = std.crypto.hash.sha2.Sha256;
const Allocator = mem.Allocator;
const Step = std.Build.Step;
const LazyPath = std.Build.LazyPath;
-const PkgConfigPkg = std.Build.PkgConfigPkg;
-const PkgConfigError = std.Build.PkgConfigError;
const RunError = std.Build.RunError;
const Module = std.Build.Module;
const InstallDir = std.Build.InstallDir;
@@ -695,119 +693,6 @@ pub fn linkLibCpp(compile: *Compile) void {
compile.root_module.link_libcpp = true;
}
-const PkgConfigResult = struct {
- cflags: []const []const u8,
- libs: []const []const u8,
-};
-
-/// Run pkg-config for the given library name and parse the output, returning the arguments
-/// that should be passed to zig to link the given library.
-fn runPkgConfig(compile: *Compile, lib_name: []const u8) !PkgConfigResult {
- const wl_rpath_prefix = "-Wl,-rpath,";
-
- const b = compile.step.owner;
- const pkg_name = match: {
- // First we have to map the library name to pkg config name. Unfortunately,
- // there are several examples where this is not straightforward:
- // -lSDL2 -> pkg-config sdl2
- // -lgdk-3 -> pkg-config gdk-3.0
- // -latk-1.0 -> pkg-config atk
- // -lpulse -> pkg-config libpulse
- const pkgs = try getPkgConfigList(b);
-
- // Exact match means instant winner.
- for (pkgs) |pkg| {
- if (mem.eql(u8, pkg.name, lib_name)) {
- break :match pkg.name;
- }
- }
-
- // Next we'll try ignoring case.
- for (pkgs) |pkg| {
- if (std.ascii.eqlIgnoreCase(pkg.name, lib_name)) {
- break :match pkg.name;
- }
- }
-
- // Prefixed "lib" or suffixed ".0".
- for (pkgs) |pkg| {
- if (std.ascii.indexOfIgnoreCase(pkg.name, lib_name)) |pos| {
- const prefix = pkg.name[0..pos];
- const suffix = pkg.name[pos + lib_name.len ..];
- if (prefix.len > 0 and !mem.eql(u8, prefix, "lib")) continue;
- if (suffix.len > 0 and !mem.eql(u8, suffix, ".0")) continue;
- break :match pkg.name;
- }
- }
-
- // Trimming "-1.0".
- if (mem.endsWith(u8, lib_name, "-1.0")) {
- const trimmed_lib_name = lib_name[0 .. lib_name.len - "-1.0".len];
- for (pkgs) |pkg| {
- if (std.ascii.eqlIgnoreCase(pkg.name, trimmed_lib_name)) {
- break :match pkg.name;
- }
- }
- }
-
- return error.PackageNotFound;
- };
-
- var code: u8 = undefined;
- const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
- const stdout = if (b.runAllowFail(&[_][]const u8{
- pkg_config_exe,
- pkg_name,
- "--cflags",
- "--libs",
- }, &code, .Ignore)) |stdout| stdout else |err| switch (err) {
- error.ProcessTerminated => return error.PkgConfigCrashed,
- error.ExecNotSupported => return error.PkgConfigFailed,
- error.ExitCodeFailure => return error.PkgConfigFailed,
- error.FileNotFound => return error.PkgConfigNotInstalled,
- else => return err,
- };
-
- var zig_cflags = std.array_list.Managed([]const u8).init(b.allocator);
- defer zig_cflags.deinit();
- var zig_libs = std.array_list.Managed([]const u8).init(b.allocator);
- defer zig_libs.deinit();
-
- var arg_it = mem.tokenizeAny(u8, stdout, " \r\n\t");
- while (arg_it.next()) |arg| {
- if (mem.eql(u8, arg, "-I")) {
- const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_cflags.appendSlice(&[_][]const u8{ "-I", dir });
- } else if (mem.startsWith(u8, arg, "-I")) {
- try zig_cflags.append(arg);
- } else if (mem.eql(u8, arg, "-L")) {
- const dir = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_libs.appendSlice(&[_][]const u8{ "-L", dir });
- } else if (mem.startsWith(u8, arg, "-L")) {
- try zig_libs.append(arg);
- } else if (mem.eql(u8, arg, "-l")) {
- const lib = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_libs.appendSlice(&[_][]const u8{ "-l", lib });
- } else if (mem.startsWith(u8, arg, "-l")) {
- try zig_libs.append(arg);
- } else if (mem.eql(u8, arg, "-D")) {
- const macro = arg_it.next() orelse return error.PkgConfigInvalidOutput;
- try zig_cflags.appendSlice(&[_][]const u8{ "-D", macro });
- } else if (mem.startsWith(u8, arg, "-D")) {
- try zig_cflags.append(arg);
- } else if (mem.startsWith(u8, arg, wl_rpath_prefix)) {
- try zig_cflags.appendSlice(&[_][]const u8{ "-rpath", arg[wl_rpath_prefix.len..] });
- } else if (b.debug_pkg_config) {
- return compile.step.fail("unknown pkg-config flag '{s}'", .{arg});
- }
- }
-
- return .{
- .cflags = try zig_cflags.toOwnedSlice(),
- .libs = try zig_libs.toOwnedSlice(),
- };
-}
-
/// Deprecated; use `compile.root_module.linkSystemLibrary(name, .{})` instead.
/// To be removed after 0.15.0 is tagged.
pub fn linkSystemLibrary(compile: *Compile, name: []const u8) void {
@@ -1225,7 +1110,7 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 {
switch (system_lib.use_pkg_config) {
.no => try zig_args.append(b.fmt("{s}{s}", .{ prefix, system_lib.name })),
.yes, .force => {
- if (compile.runPkgConfig(system_lib.name)) |result| {
+ if (compile.step.runPkgConfig(system_lib.name)) |result| {
try zig_args.appendSlice(result.cflags);
try zig_args.appendSlice(result.libs);
try seen_system_libs.put(arena, system_lib.name, result.cflags);
@@ -1967,46 +1852,6 @@ pub fn doAtomicSymLinks(
};
}
-fn execPkgConfigList(b: *std.Build, out_code: *u8) (PkgConfigError || RunError)![]const PkgConfigPkg {
- const pkg_config_exe = b.graph.env_map.get("PKG_CONFIG") orelse "pkg-config";
- const stdout = try b.runAllowFail(&[_][]const u8{ pkg_config_exe, "--list-all" }, out_code, .Ignore);
- var list = std.array_list.Managed(PkgConfigPkg).init(b.allocator);
- errdefer list.deinit();
- var line_it = mem.tokenizeAny(u8, stdout, "\r\n");
- while (line_it.next()) |line| {
- if (mem.trim(u8, line, " \t").len == 0) continue;
- var tok_it = mem.tokenizeAny(u8, line, " \t");
- try list.append(PkgConfigPkg{
- .name = tok_it.next() orelse return error.PkgConfigInvalidOutput,
- .desc = tok_it.rest(),
- });
- }
- return list.toOwnedSlice();
-}
-
-fn getPkgConfigList(b: *std.Build) ![]const PkgConfigPkg {
- if (b.pkg_config_pkg_list) |res| {
- return res;
- }
- var code: u8 = undefined;
- if (execPkgConfigList(b, &code)) |list| {
- b.pkg_config_pkg_list = list;
- return list;
- } else |err| {
- const result = switch (err) {
- error.ProcessTerminated => error.PkgConfigCrashed,
- error.ExecNotSupported => error.PkgConfigFailed,
- error.ExitCodeFailure => error.PkgConfigFailed,
- error.FileNotFound => error.PkgConfigNotInstalled,
- error.InvalidName => error.PkgConfigNotInstalled,
- error.PkgConfigInvalidOutput => error.PkgConfigInvalidOutput,
- else => return err,
- };
- b.pkg_config_pkg_list = result;
- return result;
- }
-}
-
fn addFlag(args: *std.array_list.Managed([]const u8), comptime name: []const u8, opt: ?bool) !void {
const cond = opt orelse return;
try args.ensureUnusedCapacity(1); |
Author
|
Python extensions are usually dynamically linked to the Python library, is |
Author
|
BTW, what's the equivalent of translate-c/examples/use_static_lib/build.zig Lines 18 to 25 in 115550d
|
Member
It's not, I guess it would be better to have a function that just gets the cflags with pkg-config.
|
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Writing Python extension will be a big use case for translate-c in the foreseeable future. I think it's great to include an minimal example of how to write a Python extension in Zig, to demonstrate the capabilities of
translate-cand also act as a unit test for it. This example should also act as a template project for future projects with their own Python extensions.I have crafted an example, but without comprehensive handling of Python header file paths and extension file names. Will implement this later to ensure the example works across platforms.
Any suggestions or concerns are welcome before this draft turns into a valid PR! 😃