Zig FFI与第三方C库的集成与使用
Zig的官方文档中没有对于与第三方C库集成说明太多,实际使用时,出现很多问题。
-
C的数据类型与Zig数据类型的对照。
官方有基础类型的,对于字符串,结构体,特别是指针,官方直接不建议使用!但是实际上使用
cimport
进来的很多数据类型,都是C风格的指针,需要用户自己处理!这是最大的坑点. -
C中的Union结构体,如何在zig中读取和解析
官方默认的实现是
?*anyopaque
, 不明确的指针,需要用户自己强转。 -
Zig的fmt/print相关函数,字符串格式化具体参数与说明在哪里?
直接参考文章第一小节。
这里以集成libc
和libmpv
为例子,展示一个完整的zig集成第三方C lib库遇到的坑点的解决办法!(虽然解决办法不完美,但是基本够用了)
std.fmt
针对各种print的格式化控制
https://zig.guide/standard-library/formatting-specifiers
https://zig.guide/standard-library/advanced-formatting
Linux LibC
stdio.h
/usr/include/x86_64-linux-gnu/bits/stdio.h
/usr/include/c++/12/tr1/stdio.h
/usr/include/stdio.h
zig code
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "zdemo",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibC();
b.installArtifact(exe);
// run command
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
// src/main.zig
const std = @import("std");
const io = @cImport({
@cInclude("stdio.h"); // /user/include/stdio.h
});
pub fn main() !void {
_ = io.printf("Hello, i am from C lib!\r\n");
}
重点解析:
- 所有的返回值(如printf),必须得处理,遵循zig要求
- build.zig中需要link libc(如果是其他库,要link其他库)
linux mpv
https://mpv.io/manual/stable/#list-of-input-commands
pkg-config
$ pkg-config --list-all |grep mpv
mpv mpv - mpv media player client library
$ pkg-config --cflags mpv
-I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/harfbuzz -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/fribidi -I/usr/include/x86_64-linux-gnu -I/usr/include/libxml2 -I/usr/include/lua5.2 -I/usr/include/SDL2 -I/usr/include/uchardet -I/usr/include/pipewire-0.3 -I/usr/include/spa-0.2 -D_REENTRANT -I/usr/include/libdrm -I/usr/include/sixel -I/usr/include/spirv_cross
$ pkg-config --libs mpv
-lmpv
# mpv 的头文件
$ ls /usr/include/mpv/
client.h render_gl.h render.h stream_cb.h
zig code
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "zdemo",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibC();
exe.linkSystemLibrary("mpv");
b.installArtifact(exe);
// run command
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}
// utils.zig
pub fn cstring2slice(buff: [*c]const u8) []const u8 {
var i: usize = 0;
while (buff[i] != 0) : (i += 1) {}
return buff[0..i];
}
pub fn slice2cstring(slice: [:0]u8) [*c]const u8 {
const ptr = slice.ptr;
const buff: [*c]const u8 = @constCast(ptr);
return buff;
}
// main.zig
const std = @import("std");
const libmpv = @cImport({
@cInclude("/usr/include/mpv/client.h");
});
// const libc = @cImport({
// @cInclude("stdio.h");
// });
const util = @import("utils.zig");
var g_postition: i64 = 0;
var g_duration: i64 = 0;
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
const mpv_handle = libmpv.mpv_create();
if (mpv_handle == null) {
std.debug.print("Failed to create mpv context.\n", .{});
return;
}
defer libmpv.mpv_destroy(mpv_handle);
// propertys
_ = libmpv.mpv_set_property_string(mpv_handle, "vid", "no");
_ = libmpv.mpv_set_property_string(mpv_handle, "input-default-bindings", "yes");
// events
_ = libmpv.mpv_observe_property(mpv_handle, 0, "time-pos", libmpv.MPV_FORMAT_INT64);
_ = libmpv.mpv_observe_property(mpv_handle, 0, "duration", libmpv.MPV_FORMAT_INT64);
_ = libmpv.mpv_request_log_messages(mpv_handle, "no"); // info
if (libmpv.mpv_initialize(mpv_handle) < 0) {
std.debug.print("Failed to initialize mpv context.\n", .{});
return;
}
// load file
{
const cmd = [_][*c]const u8{
"loadfile", // MPV 命令
"/home/andy/音乐/test/1.mp3", // 文件名
null, // 结束符 NULL
};
const cmd_ptr: [*c][*c]const u8 = @constCast(&cmd);
_ = libmpv.mpv_command(mpv_handle, cmd_ptr);
}
// wait for events
while (true) {
const event = libmpv.mpv_wait_event(mpv_handle, 0).*; // 解引用*c的指针到zig struct
if (event.event_id == libmpv.MPV_EVENT_NONE) continue;
// const e_name = libmpv.mpv_event_name(event.event_id);
// _ = libc.printf("event: %d => %s\n", event.event_id, e_name);
switch (event.event_id) {
libmpv.MPV_EVENT_PROPERTY_CHANGE => {
const prop: *libmpv.mpv_event_property = @ptrCast(@alignCast(event.data));
const name = util.cstring2slice(prop.name);
// std.debug.print("property: {s} format: {d}\n", .{ name, prop.format });
// _ = libc.printf("prop: %s\r\n", name);
if (prop.format == libmpv.MPV_FORMAT_INT64 and std.mem.eql(u8, name, "time-pos")) {
if (prop.data) |ptr| {
const time_pos: *i64 = @ptrCast(@alignCast(ptr));
g_postition = time_pos.*;
var percent: f32 = 0.0;
if (g_duration > 0) {
percent = @as(f32, @floatFromInt(g_postition)) / @as(f32, @floatFromInt(g_duration)) * 100.0;
}
std.debug.print("progress: {} - {} => {d:.1}\n", .{ g_postition, g_duration, percent });
}
}
if (std.mem.eql(u8, name, "duration")) {
if (prop.data) |ptr| {
const duration: *i64 = @ptrCast(@alignCast(ptr));
g_duration = duration.*;
std.debug.print("duration: {d}\n", .{duration.*});
}
}
},
libmpv.MPV_EVENT_LOG_MESSAGE => {
const msg: *libmpv.mpv_event_log_message = @ptrCast(@alignCast(event.data));
const text: [*]const u8 = @constCast(msg.text);
const level: [*]const u8 = @constCast(msg.level);
std.debug.print("log: {s} => {s}\n", .{ util.cstring2slice(level), util.cstring2slice(text) });
// _ = libc.printf("log: %s => %s\n", level, text);
},
libmpv.MPV_EVENT_FILE_LOADED => {
std.debug.print("file loaded.\n", .{});
{
// parse metadata
// https://mpv.io/manual/stable/#command-interface-metadata
var meta_count: i64 = 0;
_ = libmpv.mpv_get_property(mpv_handle, "metadata/list/count", libmpv.MPV_FORMAT_INT64, &meta_count);
// std.debug.print("metadata count: {d}\n", .{meta_count});
for (0..@as(usize, @intCast(meta_count))) |i| {
const kk = try std.fmt.allocPrintZ(allocator, "metadata/list/{d}/key", .{i});
const vv = try std.fmt.allocPrintZ(allocator, "metadata/list/{d}/value", .{i});
// std.debug.print("metadata keys: {s} => {s}\n", .{ kk, vv });
const k = libmpv.mpv_get_property_string(mpv_handle, util.slice2cstring(kk));
const v = libmpv.mpv_get_property_string(mpv_handle, util.slice2cstring(vv));
std.debug.print("metadata: {s} => {s}\n", .{ util.cstring2slice(k), util.cstring2slice(v) });
}
}
{
// seek to 90%
const cmd = [_][*c]const u8{
"seek", // MPV 命令
"50",
"absolute-percent",
null, // 结束符 NULL
};
const cmd_ptr: [*c][*c]const u8 = @constCast(&cmd);
_ = libmpv.mpv_command(mpv_handle, cmd_ptr);
}
},
libmpv.MPV_EVENT_END_FILE => {
std.debug.print("file end.\n", .{});
loadfile(mpv_handle, "/home/andy/音乐/test/1.mp3");
},
libmpv.MPV_EVENT_SHUTDOWN => {
std.debug.print("shutdown.\n", .{});
break;
},
else => {
std.debug.print("null process event: {d}\n", .{event.event_id});
},
}
}
}
fn loadfile(mpv_handle: ?*libmpv.mpv_handle, filename: []const u8) void {
// load file
const cmd = [_][*c]const u8{
"loadfile", // MPV 命令
@as([*c]const u8, @constCast(filename.ptr)), // 文件名
null, // 结束符 NULL
};
const cmd_ptr: [*c][*c]const u8 = @constCast(&cmd);
_ = libmpv.mpv_command(mpv_handle, cmd_ptr);
}
难点解析:
[*c]const u8, [*]const u8, []const u8, []u8
他们之间的互相转换。- 对于C库,都需要转为
[*c]const u8
提供字符串。 - 对于zig,都需要转为
[]u8, []const u8
提供字符串。 - 转换的重点是
@as(xxx, @constCast(yy))
. zig中的slice有一个ptr的指针,可以直接使用,或者使用slice首个元素的地址/数组地址。
- 对于C库,都需要转为
[*c][*c]const u8
如何初始化?- 没有初始化的办法,只能通过数组做类型转换
- 先构造
[_][*c]const u8
的数组,然后对数组指针进行类型转换[*c][*c]const u8
。 - 最后一个元素一定要是
null
, 防止C数组越界。
- C语言的
union
对应zig的?*anyopaque
,如何提取数据?- 通过强转zig中的指针,
var a:*i64 = @ptrCast(@alignCast(data_ptr)); var b = a.*;
这样子就可以得到。本质上还是指针的类型转换,然后加上zig的解引用。
- 通过强转zig中的指针,
- 对于复合错误类型的返回值,可以使用
try
直接取值。 - 对于
?*type
可以使用if(xx) | x | {}
解开使用。 - 字符串格式化,与C差别很大,需要参考zig guide 中相关章节。zig官方文档没有介绍。
- https://zig.guide/standard-library/formatting-specifiers
- https://zig.guide/standard-library/advanced-formatting
- libmpv文档也很少,需要参考
- https://mpv.io/manual/stable/#command-interface-metadata
- https://github.com/mpv-player/mpv-examples/blob/master/libmpv/qt/qtexample.cpp