Zig FFI与第三方C库的集成与使用

Zig FFI与第三方C库的集成与使用

Zig的官方文档中没有对于与第三方C库集成说明太多,实际使用时,出现很多问题。

  1. C的数据类型与Zig数据类型的对照。

    官方有基础类型的,对于字符串,结构体,特别是指针,官方直接不建议使用!但是实际上使用cimport进来的很多数据类型,都是C风格的指针,需要用户自己处理!这是最大的坑点.

  2. C中的Union结构体,如何在zig中读取和解析

    官方默认的实现是?*anyopaque, 不明确的指针,需要用户自己强转。

  3. Zig的fmt/print相关函数,字符串格式化具体参数与说明在哪里?

    直接参考文章第一小节。

这里以集成libclibmpv为例子,展示一个完整的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]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的解引用。
  • 对于复合错误类型的返回值,可以使用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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值