Взаимодействие с C

2025-04-20 3941 19

Одним из ключевых преимуществ языка Zig является его простота и эффективность при взаимодействии с кодом, написанным на C. Это делает Zig отличным выбором как для постепенной миграции существующих проектов с C, так и для интеграции с уже проверенными и широко используемыми C-библиотеками. Благодаря этому вы можете использовать Zig в реальных проектах, не начиная всё с нуля.

Вот несколько вариантов использования кода на C в Zig:

Хотя взаимодействие с C-кодом не является уникальной возможностью Zig — большинство языков реализуют такую поддержку через механизм FFI (Foreign Function Interface) — Zig предлагает особенно удобные и мощные средства для такой интеграции. Он позволяет подключать C-код напрямую, без необходимости писать промежуточный код на C или использовать отдельные инструменты для связывания.

Для успешной работы с C-кодом на уровне бинарного взаимодействия необходимо соблюдать требования стандартного ABI (Application Binary Interface) — набора правил, определяющих, как функции и данные взаимодействуют на уровне машинного кода. Чтобы Zig-код мог корректно работать с функциями и структурами из C, необходимо учитывать следующие аспекты:

Если все эти условия выполнены, вы сможете без проблем вызывать C-функции из Zig, использовать C-структуры и даже экспортировать Zig-функции, чтобы они были доступны из C-кода.

Давайте теперь подробнее рассмотрим каждый аспект соответсвия с-ABI со стороны Zig.

Соответствие типов данных

Чтобы наш код на Zig мог корректно взаимодействовать с кодом на C, в первую очередь необходимо обеспечить совместимость типов данных. Ведь именно через них осуществляется обмен информацией между языками. Если типы данных не совпадают по размеру, выравниванию или представлению в памяти, это может привести к неожиданным ошибкам на этапе выполнения или, что ещё хуже, к тихим багам.

Zig предоставляет специальный набор типов данных, которые по своей структуре и поведению полностью совместимы с C:

C тип Zig тип Описание
int c_int 32-битное целое число
char c_char 8-битный символ
unsigned int c_uint 32-битное беззнаковое целое число
long c_long 64-битное целое число
unsigned long c_ulong 64-битное беззнаковое целое число
long long c_longlong 64-битное целое число
unsigned long long c_ulonglong 64-битное беззнаковое целое число
void anyopaque Пустой тип
float c_float 32-битное число с плавающей точкой
double c_double 64-битное число с плавающей точкой

Несмотря на то что Zig предоставляет отдельный набор типов, совместимых с C, при использовании примитивных типов данных компилятор Zig автоматически приводит их к соответствующим C-типам при необходимости. Это означает, что вам не нужно вручную заботиться о приведении типов при передаче данных в C-функции — Zig сам позаботится о корректном соответствии по размеру, выравниванию и представлению данных.

Благодаря этому взаимодействие с C-кодом на уровне примитивных типов становится простым и прозрачным, что значительно упрощает интеграцию между языками.

Однако, как только мы выходим за рамки примитивных типов, ситуация становится немного сложнее. Особенно это касается таких типов, как срезы (slices), строки и структуры (structs).

Когда мы рассматривали строки и срезы в предыдущих главах, мы упоминали, что Zig поддерживает строки в стиле C — то есть null-терминированные массивы символов. Более того, Zig умеет автоматически приводить строковые литералы как к строкам в формате Zig ([]const u8), так и к C-строкам ([*:0]const u8), когда это необходимо.

Рассмотрим следующий пример, демонстрирующий, как происходит такое приведение строк при вызове C-функции:

const std = @import("std");

pub fn main() void {
    _ = std.c.printf("Привет, мир!");
}

В этом примере строковый литерал “Привет, мир!” используется как обычная строка Zig ([]const u8), но при передаче в функцию printf он автоматически преобразуется в C-строку ([*:0]const u8), которую printf и ожидает.

А теперь давайте немного изменим наш пример и посмотрим, что произойдёт, если мы попробуем передать в printf строку в стиле Zig напрямую:

const std = @import("std");

pub fn main() void {
    const zigString: []const u8 = "Привет, мир!";
    _ = std.c.printf(zigString);
}

Если мы запустим данный код мы получим следующий вывод:

$ zig build run
run
└─ run simple
   └─ zig build-exe simple Debug native 1 errors
src/main.zig:8:18: error: expected type '[*c]const u8', found '[]const u8'
    _ = c.printf(zigString);
                 ^~~~~~~~~

Как мы видим, компиляция завершилась с ошибкой: наши типы данных не совпадают — функция ожидает строку в стиле C ([*c]const u8), а мы пытаемся передать ей строку в стиле Zig ([]const u8).

Zig намеренно избегает неявных преобразований типов и вместо этого сообщает о несоответствии на этапе компиляции, ожидая, что разработчик сам явно устранит ошибку.

Чтобы исправить эту проблему, нам нужно вручную привести один тип к другому — в данном случае, например, с помощью функции @ptrCast:

const std = @import("std");

pub fn main() void {
    const zigString: []const u8 = "Привет, мир!";
    _ = std.c.printf(@ptrCast(zigString));
}

Теперь наш пример успешно компилируется и работает. При необходимости передачи массива из Zig в C вы можете действовать аналогичным образом.

Теперь давайте рассмотрим передачу более сложных типов данных, таких как структуры. На самом деле, здесь всё довольно просто. В главе про выравнивание мы уже упоминали использование внешних (extern) структур, которые можно применять для обмена данными между Zig и C.

Давайте вспомним как создаются такие структуры:

// Структура, совместимая с C
export const Point = extern struct {
    x: c_int,
    y: c_int,
};

export fn create_point(x: c_int, y: c_int) Point {
    return Point{ .x = x, .y = y };
}

Для того чтобы создать структуру, совместимую с C, достаточно просто пометить её как extern и использовать в ней типы данных, совместимые с C.

Префикс extern, указанный перед определением структуры, изменяет правила выравнивания и порядок размещения полей в соответствии с соглашениями C, что обеспечивает корректную совместимость на уровне ABI.

В нашем примере мы создаём структуру точки (Point) в двумерном пространстве и определяем функцию, совместимую с C, которая возвращает экземпляр этой структуры.

Но что если мы хотим использовать в коде на Zig структуру определенную в C коде. Давайте рассмотрим уже знакомую нам структуру пользователя (User) с двумя полями id и name и на этот раз будем использовать структуру User определенную в C коде. Для начала давайте рассмотрим как выглядит наша структура:

typedef struct {
    int id;
    char name[100];
} User;

Как мы видим id имеет тип данных целого числа, а name - массив символов длиной 100, что по сути является строкой длиной в 100 символов. Теперь давайте используем нашу структуру в коде на Zig:

const std = @import("std");
const c = @cImport({
    @cInclude("user.h");
});

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var user: c.User = undefined;
    user.id = 1;

    var name = try allocator.alloc(u8, 6);
    defer allocator.free(name);

    @memcpy(name[0..(name.len - 1)], "alice");
    name[name.len - 1] = 0;

    const copy_len = @min(name.len, 100);
    @memcpy(user.name[0..copy_len], name[0..copy_len]);

    _ = std.c.printf("User ID: %d\n", user.id);
    _ = std.c.printf("User Name: %s\n", &user.name);
}

Первое, что появилось нового в нашем коде — это использование директив @cImport и @cInclude. Блок @cImport предназначен для подключения C-кода в программу на Zig. По сути, @cImport открывает специальный блок, внутри которого можно использовать такие директивы, как @cInclude для подключения заголовочных файлов и @cDefine для задания C-препроцессорных макросов.

Однако просто импортировать заголовочный файл user.h недостаточно — компилятор Zig не сможет собрать проект, если не будет знать, где искать этот файл.

Чтобы компиляция прошла успешно, необходимо явно указать путь к папке, в которой находится user.h. Для этого в файл build.zig нужно добавить всего одну строку:

...
const exe_mod = b.createModule(.{
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});

exe_mod.addIncludePath(b.path("src")); // Добавляем вот эту строчку
...

Эта строка добавляет папку с исходным кодом в список путей для поиска заголовочных файлов. После этого компилятор сможет найти user.h, и сборка пройдет успешно.

Теперь давайте запустим наш код — и, если всё настроено правильно, мы увидим, что программа работает как ожидалось.

$ zig build run
User ID: 1
User Name: alice

Как видно из нашего примера, при работе со структурами из C существует ряд нюансов. Например, при объявлении структуры мы присваиваем ей значение undefined, чтобы указать, что память под эту структуру не инициализирована и может содержать произвольные данные.

Затем мы вручную заполняем поля структуры. В частности, для строкового поля (имени) нам необходимо самостоятельно выделить память и скопировать в неё данные из строки, поскольку C-структуры, как правило, не работают со срезами Zig — они ожидают указатель на заранее выделенный буфер. Кроме того, при создании C-строки важно не забыть вручную добавить нулевой байт (0) в конец строки, чтобы она соответствовала формату null-terminated, принятому в языке C.

В целом, при работе с C-структурами в Zig важно быть особенно внимательным: нужно учитывать особенности управления памятью, ручную инициализацию полей, а также соответствие типов и выравнивания. Однако вам довольно редко прийдется инициализировать вручную С-структуры, так как обычно в C коде есть специальные функции, которые сами инициализируют все объекты за вас. Давайте рассмотрим пример:

const c = @cImport({
    @cInclude("time.h");
    @cInclude("stdio.h");
});

pub fn main() void {
    var tm: c.struct_tm = undefined;
    var time_val: c.time_t = undefined;

    // Get the current time and store it in time_val
    _ = c.time(&time_val);

    // Pass the address of time_val to localtime_r
    _ = c.localtime_r(&time_val, &tm);

    // Print full time data
    _ = c.printf("Date: %d-%02d-%02d\n", tm.tm_year + 1900, // Year (tm_year is years since 1900)
        tm.tm_mon + 1, // Month (tm_mon is 0-11)
        tm.tm_mday // Day of the month (1-31)
    );

    _ = c.printf("Time: %02d:%02d:%02d\n", tm.tm_hour, // Hour (0-23)
        tm.tm_min, // Minute (0-59)
        tm.tm_sec // Second (0-59)
    );

    // Get weekday name
    const weekday_names = [_][]const u8{ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
    const weekday = weekday_names[@intCast(tm.tm_wday)];

    _ = c.printf("Weekday: %s\n", weekday.ptr);

    // Print day of year (0-365)
    _ = c.printf("Day of year: %d\n", tm.tm_yday + 1); // tm_yday is 0-365
}

Данный код выведет:

Date: 2025-04-22
Time: 09:51:42
Weekday: Tuesday
Day of year: 112

Как мы видим для инициализации структур с текущей датой и временем мы использовали C функции, такие как time, localtime_r.

Соглашение о вызовах

Мы рассмотрели как передавать типы данных между Zig и C чтобы они были совместимы, теперь нам осталось рассмотреть как вызывать функции в совместимом с С соглашении. Нам может потребоваться передать в функцию на языке C функцию из языка Zig. Давайте напишем пример такой задачи и потом рассмотрим как же обеспечивается совместимость вызова функций:

const std = @import("std");

const c = @cImport({
    @cInclude("stdlib.h");
    @cInclude("string.h");
});

fn compare(a: ?*const anyopaque, b: ?*const anyopaque) callconv(.C) c_int {
    const str_a: [*:0]const u8 = @ptrCast(@alignCast(a.?));
    const str_b: [*:0]const u8 = @ptrCast(@alignCast(b.?));
    return c.strcmp(str_a, str_b);
}

pub fn main() void {
    const strings = [_][*:0]const u8{ "hello", "world", "zig", "abc" };

    c.qsort(
        @ptrCast(@constCast(&strings)),
        strings.len,
        @sizeOf([*:0]const u8),
        compare,
    );

    for (strings) |s| {
        _ = std.c.printf("%s\n", s);
    }
}

В нашем примере мы используем функцию быстрой сортировки (qsort) из стандартной библиотеки C для сортировки массива строк.

Функция qsort требует на вход указатель на функцию сравнения, которая должна принимать два указателя на элементы массива и возвращать значение типа c_int. В нашем случае функция compare принимает два указателя на неизвестный тип (anyopaque) и использует strcmp из стандартной библиотеки C для сравнения строк.

Чтобы функция compare была совместима с вызовом из C-кода, мы явно указываем соглашение о вызовах в стиле C с помощью callconv(.C). Внутри нашей функции мы явно приводим указатели к типу [*:0]const u8 и используем strcmp для сравнения строк.

Мы уже рассмотрели, как обеспечить соответствие соглашениям ABI при взаимодействии C-кода и кода на Zig. Теперь давайте подробнее разберёмся, как происходит импорт и компиляция C-кода в Zig.

Сборка C кода

Давайте начнем с того, что разберемся как с помощью компилятора Zig собирать C код. Zig включает в себя встроенный компилятор C, что позволяет напрямую компилировать и запускать C-программы без необходимости устанавливать сторонние инструменты, такие как GCC или Clang. Это особенно удобно при переносе проектов или для быстрой компиляции отдельных C-файлов.

Рассмотрим следующий простой пример на C, и попробуем скомпилировать его с помощью компилятора Zig:

// Файл math.h
#ifndef MATH_H
#define MATH_H

#ifndef STEP_BY
  #define STEP_BY 2
#endif

int* range(int start, int end, int* length);

#endif
// Файл math.c
#include <stdlib.h>
#include "math.h"

int* range(int start, int end, int* length) {
  if (STEP_BY == 0) {
      *length = 0;
      return NULL;
  }

  int len = 0;
  if ((STEP_BY > 0 && start <= end) || (STEP_BY < 0 && start >= end)) {
      len = ((end - start) / STEP_BY) + 1;
  }

  int* arr = (int*)malloc(len * sizeof(int));
  if (arr == NULL) {
      *length = 0;
      return NULL;
  }

  for (int i = 0; i < len; i++) {
      arr[i] = start + (i * STEP_BY);
  }

  *length = len;
  return arr;
}
// Файл main.c
#include <stdio.h>
#include <stdlib.h>
#include "math.h"

int main() {
  int start = 10;
  int end = 20;
  int length = 0;

  int* result = range(start, end, &length);

  if (result != NULL) {
      printf("Range from %d to %d with step %d:\n", start, end, STEP_BY);
      for (int i = 0; i < length; i++) {
          printf("%d ", result[i]);
      }
      printf("\n");

      free(result);
  } else {
      printf("Failed to create range or invalid parameters.\n");
  }

  return 0;
}

Наш код определяет функцию range, которая создает массив целых чисел в заданном диапазоне с заданным шагом. Функция возвращает указатель на массив и его длину. Далее мы выводим получившийся массив пользователю. Давайте теперь скомпилируем и запустим нашу программу:

$ zig cc ./src/math.c ./src/main.c -o math
$ ./math

В результате мы увидим следующий вывод:

Range from 10 to 20 with step 2:
10 12 14 16 18 20

Как мы видим компилятор Zig позволяет нам успешно компилировать C код. Если вам необходимо собрать код под конкретную платформу или архитектуру, вы можете использовать опции компилятора Zig для этого:

zig cc ./src/math.c ./src/main.c -o math -target x86_64-linux-gnu

Эта команда соберёт нашу программу для архитектуры x86_64 под Linux. Однако гораздо удобнее интегрировать сборку C-кода прямо в сборочную систему Zig, чтобы не приходилось каждый раз вручную указывать опции компилятора.

Такой подход особенно полезен в проектах, где используются как Zig-, так и C-файлы, а также при необходимости подключения внешних заголовочных файлов или C-библиотек. Использование файла build.zig позволяет централизованно управлять сборочным процессом, упростить настройку зависимостей и повысить переносимость проекта между различными платформами.

Пример конфигурации сборки с использованием build.zig может выглядеть следующим образом:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe_mod = b.addModule("main", .{
        .target = target,
        .optimize = optimize,
        .link_libc = true, // добавляем поддержку libc
    });

    exe_mod.addIncludePath(b.path("src"));
    exe_mod.addCSourceFiles(.{ .files = &.{ "src/main.c", "src/math.c" }, .flags = &.{"-std=c11"} });

    const exe = b.addExecutable(.{
        .name = "math",
        .root_module = exe_mod,
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

Теперь если мы выполним команду zig build run, то наша программа успешно соберется и запустится.

Импортирование C-кода

Теперь давайте рассмотрим, каким образом можно импортировать C-код в программу, написанную на языке Zig. Для этого мы воспользуемся уже готовым кодом на языке C и перепишем файл main.c, реализовав его функциональность на Zig.

В результате у нас получится файл main.zig, в котором будет производиться вызов функций, определённых в C-коде. Ниже представлен финальный вид этого файла:

const std = @import("std");

const c = @cImport({
    @cInclude("stdlib.h");
    @cInclude("math.c");
});

pub fn main() !void {
    const start: i32 = 10;
    const end: i32 = 20;
    var length: i32 = 0;

    const result = c.range(start, end, &length);
    if (result == null) {
        std.debug.print("Failed to create range or invalid parameters.\n", .{});
    } else {
        std.debug.print("Range from {d} to {d} with step {d}:\n", .{ start, end, c.STEP_BY });

        const result_slice = result[0..@intCast(length)];

        for (result_slice) |value| {
            std.debug.print("{d} ", .{value});
        }
        std.debug.print("\n", .{});

        c.free(result);
    }
}

Первое, что необходимо сделать при использовании C-кода в программе на Zig — это импортировать его. Для этого в Zig предусмотрена специальная директива @cImport, которая позволяет подключать C-заголовочные файлы и использовать C-функции непосредственно в Zig-коде.

Как уже упоминалось внутри блока @cImport можно использовать директиву @cInclude для подключения конкретных заголовочных файлов. Это аналогично директиве #include в языке C.

В нашем случае необходимо подключить заголовочный файл math.h, поскольку мы планируем использовать определённую в нём функцию range. Также нам нужно подключить stdlib.h, так как в программе присутствует работа с динамически выделенной памятью, которую нужно освободить с помощью функции free.

Дальше у нас происходит вызов нашей функции range из заголовочного файла math.h и преобразование результата в массив нужной длины. После вывода результат пользователю, нам необходимо освободить выделенную память с помощью функции free.

Для того, чтобы скомпилировать и запустить нашу программу нам необходимо внести изменения в файл build.zig, добавив в него соответствующие команды для компиляции и запуска программы:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe_mod.addIncludePath(b.path("src"));

    const exe = b.addExecutable(.{
        .name = "simple",
        .root_module = exe_mod,
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_unit_tests = b.addTest(.{
        .root_module = exe_mod,
    });

    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_exe_unit_tests.step);
}

Наш сборочный файл изменился незначительно: теперь вместо сборки файла main.c мы указываем путь к файлу main.zig в качестве корневого модуля. Однако для успешной сборки программы нам по-прежнему необходимо указать путь к заголовочным файлам на языке C.

Есть еще один момент, который стоит упомянуть, прежде чем мы двинемся дальше. В секции @cImport мы можем использовать директиву @cDefine для того, чтобы переопределять макросы из заголовочных файлов на языке C. Давайте изменим директиву @cImport в нашем файле на следующий и посмотрим как изменится вывод нашей программы:

const c = @cImport({
    @cInclude("stdlib.h");
    @cDefine("STEP_BY", "4");

    @cInclude("math.c");
});
...

Теперь если мы запустим нашу программу, то увидим, что шаг с которым мы итерируемся по числам изменился:

Range from 10 to 20 with step 4:
10 14 18

Использование C библиотеки

Еще один из вариантов использования C библиотеки в Zig - это использование функций из статической библиотеки. Для того чтобы продемонстрировать как работать со статическими библиотеками нам необходимо изменить два файла - файл сборки и наш файл main.zig. Давайте начнем с файла сборки build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const lib = b.addStaticLibrary(.{
        .name = "math",
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });

    lib.addCSourceFiles(.{ .files = &.{"src/math.c"}, .flags = &.{"-std=c11"} });
    lib.addIncludePath(b.path("src"));

    b.installArtifact(lib);

    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });

    const exe = b.addExecutable(.{
        .name = "simple",
        .root_module = exe_mod,
    });

    exe.linkLibrary(lib);

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_unit_tests = b.addTest(.{
        .root_module = exe_mod,
    });

    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_exe_unit_tests.step);
}

В файл сборки мы внесли следующие изменения. Сначала добавляется сборка нашей статической библиотеки с использованием возможностей Zig по сборке C-кода. Затем, при сборке основного приложения, мы подключаем (линкуем) эту статическую библиотеку с помощью функции linkLibrary. И это все изменения, которые нужно сделать в файле build.zig. Теперь перейдем к тому как будет выглядеть наш файл main.zig:

const std = @import("std");

extern fn range(start: i32, end: i32, length: *i32) ?[*]i32;

pub fn main() !void {
    const start: i32 = 10;
    const end: i32 = 20;
    var length: i32 = 0;

    const result = range(start, end, &length);
    if (result == null) {
        std.debug.print("Failed to create range or invalid parameters.\n", .{});
    } else {
        std.debug.print("Range from {d} to {d} with step {d}:\n", .{ start, end, 2 });

        const result_slice = result.?[0..@intCast(length)];

        for (result_slice) |value| {
            std.debug.print("{d} ", .{value});
        }
        std.debug.print("\n", .{});

        std.c.free(result);
    }
}

Мы убрали из нашего файла импортирование C кода, и вместо этого описали нашу функцию range как внешнюю функцию, используя ключевое слово extern. Остальной код практически не изменился, разве что теперь для освобождения памяти мы используем функцию std.c.free вместо std.free.

Испрользование Zig кода в C

Итак нам осталось рассмотреть еще один вариант использования интеграции Zig и кода на языке C - использования Zig библиотеки в C-коде. Для демонстрации этой возможности нам прийдется инвертировать наш пример и перенести нашу функцию range в Zig код, а использование ее перенести в C код. Давайте посмотрим сначала как нам реализовать библиотеку на Zig:

const std = @import("std");

export fn zig_range(start: i32, end: i32, step: i32, len: *usize) [*]i32 {
    if (step == 0) {
        len.* = 0;
        return undefined;
    }

    len.* = if ((step > 0 and start <= end) or (step < 0 and start >= end))
        @intCast(@divTrunc(end - start, step) + 1)
    else
        0;

    const allocator = std.heap.c_allocator;
    const slice = allocator.alloc(i32, len.*) catch {
        len.* = 0;
        return undefined;
    };

    for (slice, 0..) |*val, i| {
        val.* = start + @as(i32, @intCast(i)) * step;
    }

    return slice.ptr;
}

export fn zig_free_range(ptr: [*]i32, len: usize) void {
    const allocator = std.heap.c_allocator;
    const slice = ptr[0..len];
    allocator.free(slice);
}

В целом в коде нашей библиотеки на Zig нет ничего такого, чтобы мы не рассматривали ранее. Основной момент это использование ключевого слова extern для объявления функций, которые будут использоваться в C-коде. Теперь давайте соберем нашу библиотеку на Zig и для этого нам надо выполнить команду:

zig build-lib ./src/ziglib.zig -static -OReleaseSafe

В результате у нас появится два файла libziglib.a и libziglib.a.o. Это наша статическая библиотека, которую мы можем использовать в C-коде. Теперь рассмотрим как будет выглядеть на код на C:

// Файл ziglib.h
#ifndef ZIGLIB_H
#define ZIGLIB_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

int32_t* zig_range(int32_t start, int32_t end, int32_t step, size_t* length);
void zig_free_range(int32_t* array, size_t length);

#ifdef __cplusplus
}
#endif

#endif // ZIGLIB_H
// Файл main.c
#include <stdio.h>
#include "ziglib.h"

int main() {
    size_t length;
    int32_t* numbers = zig_range(10, 20, 2, &length);

    if (numbers && length > 0) {
        printf("Range from Zig:\n");
        for (size_t i = 0; i < length; i++) {
            printf("%d ", numbers[i]);
        }
        printf("\n");

        zig_free_range(numbers, length);
    } else {
        printf("Failed to generate range\n");
    }

    return 0;
}

Для того чтобы использовать нашу библиотеку мы формируем файл ziglib.h в котором описываем сигнатуры наших функций, а дальше просто используем наши функции из статической библиотеки в нашем коде на языке C. Для того чтобы скомпилировать наш проект с помощью gcc необходимо выполнить следующие команду:

gcc ./src/main.c -o main -L. -lziglib -I.

После этого у нас появится файл main, при запуске которого мы увидим уже знакомый вывод нашей программы.

Использование translate-c

В инструментарии компилятора Zig есть полезная команда, позволяющая транслировать C-код в Zig. Теоретически, имея исходный код на языке C, вы можете воспользоваться командой zig translate-c, указать необходимые параметры, и на выходе получить эквивалентный Zig-код, который можно использовать в своём проекте.

Однако на практике результат не всегда оказывается пригодным для немедленного использования. Лично я решил не акцентировать внимание на этой команде, поскольку мои попытки использовать её приводили к тому, что сгенерированный код получался весьма «грязным» — с огромным количеством лишнего и трудно читаемого кода, удаление которого никак не влияло на работоспособность.

Важно отметить, что при использовании директивы @cImport в Zig, под капотом происходит тот же процесс трансляции C-кода, что и при явном вызове zig translate-c. Только этот процесс выполняется автоматически во время компиляции и более оптимизирован для реального использования. Таким образом, @cImport — это более удобный и практичный способ интеграции C-кода в проект на Zig, без необходимости разбираться с громоздким автоматически сгенерированным кодом.

Заключение

В этой главе мы рассмотрели различные аспекты взаимодействия между Zig и C, который является одной из сильных сторон языка Zig. Такое взаимодействие позволяет разработчикам использовать огромное количество существующих C-библиотек и постепенно мигрировать с C на Zig, не переписывая весь код сразу.

Важно отметить, что, хотя работа с C-кодом в Zig достаточно проста, она все же требует понимания особенностей взаимодействия на уровне ABI и внимательного отношения к управлению памятью, особенно при работе с C-строками и структурами данных.

В целом, возможность бесшовной интеграции с C является одним из ключевых преимуществ Zig, делающим его практичным выбором для разработки системного программного обеспечения, особенно в проектах, где уже используются компоненты на C или требуется взаимодействие с существующими C-библиотеками.

#build #c #zig #zigbook

0%