Skip to content

Вызов C из Mojo

This content is not available in your language yet.

На C написано почти всё, что лежит под современными программами: системные вызовы, сжатие, шифрование, работа с изображениями и звуком. Переписывать это незачем — Mojo умеет звать C напрямую.

В отличие от Python, здесь нет интерпретатора посередине, и вызов стоит почти ничего. Зато и страховки нет почти никакой — об этом большая часть главы.

Стандартная библиотека C уже подключена к любой программе на Mojo, и звать её функции можно без подготовки:

from std.ffi import external_call, c_int, c_double
def main():
print(external_call["abs", c_int](c_int(-7)))
print(external_call["sqrt", c_double](c_double(2.0)))
print(external_call["getpid", c_int]() > 0)
Результат
7
1.4142135623730951
True

В квадратных скобках — имя функции и тип результата, в круглых — аргументы. Типы аргументов компилятор выводит сам из того, что вы передали.

В C у строки нет длины. Строка — это адрес первого байта, а конец отмечен нулевым байтом. Функция вроде strlen просто идёт по памяти, пока не наткнётся на ноль.

У String в Mojo для этого есть специальный метод — он гарантирует, что ноль на месте:

from std.ffi import external_call, c_size_t
def main():
var word = String("привет")
var n = external_call["strlen", c_size_t](word.as_c_string_span().ptr())
print(n, word.byte_length())
Результат
12 12

Двенадцать, а не шесть: C считает байты, а каждая кириллическая буква в UTF-8 занимает два. Ровно то же самое говорит byte_length() — подробно об этом в главе Строки.

Вот место, где легко получить неправильный ответ без единой ошибки. Срез строки в Mojo не копирует данные — он указывает внутрь исходной строки. И нуля после него нет:

from std.ffi import external_call, c_size_t
def main():
var text = String("abcdef")
var view = text[byte=0:3]
print(view, view.byte_length())
print(external_call["strlen", c_size_t](view.unsafe_ptr()))
Результат
abc 3
6

Срез — три байта. А C насчитал шесть: после abc нуля нет, и strlen пошёл дальше, в def, пока не нашёл ноль в конце исходной строки.

Здесь повезло: ноль нашёлся в пределах своей памяти. В другой раз C прочитает чужие данные — или программа упадёт.

Функции libc доступны сразу. Для остальных библиотек её нужно загрузить. Пусть есть файл vec.c:

#include <stddef.h>
double dot(const double *a, const double *b, size_t n) {
double s = 0.0;
for (size_t i = 0; i < n; i++) s += a[i] * b[i];
return s;
}
int add(int a, int b) { return a + b; }

Собираем его в разделяемую библиотеку:

Окно терминала
gcc -O2 -shared -fPIC vec.c -o libvec.so

И зовём из Mojo:

from std.ffi import OwnedDLHandle, c_int, c_double, c_size_t
def use_library() raises:
var lib = OwnedDLHandle("./libvec.so")
var add = lib.get_function[c_int]("add")
print(add(c_int(2), c_int(3)))
var a: List[Float64] = [1.0, 2.0, 3.0]
var b: List[Float64] = [4.0, 5.0, 6.0]
var dot = lib.get_function[c_double]("dot")
print(dot(a.unsafe_ptr(), b.unsafe_ptr(), c_size_t(3)))
5
32.0

Массивы передаются так же, как в C: указателем на первый элемент и отдельно длиной. List хранит числа подряд, без обёрток, поэтому его unsafe_ptr() — ровно то, чего ждёт функция на C.

Вот главное, что нужно знать о вызове C. Mojo не видит исходник функции на C и не может проверить, правильно ли вы описали её тип. Что вы написали — тому он и верит.

Функция dot возвращает double. Ошибёмся и объявим int:

var dot = lib.get_function[c_int]("dot") # на самом деле double
print(dot(a.unsafe_ptr(), b.unsafe_ptr(), c_size_t(3)))
3

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

На x86-64 целый результат функция кладёт в регистр rax, дробный — в xmm0. Функция dot честно положила ответ в xmm0, а мы попросили прочитать rax. Что же там лежит? Дизассемблер показывает цикл внутри dot:

xor %eax,%eax ; i = 0 — счётчик цикла живёт в rax
...
add $0x1,%rax ; i += 1
cmp %rax,%rdx ; i == n ?
jne ...
movapd %xmm1,%xmm0 ; ответ — в xmm0

После цикла счётчик i равен n, то есть трём. Его мы и прочитали.

И это число — не свойство программы, а случайность сборки. Та же библиотека, собранная с другими флагами:

Флаги gccn = 3n = 5n = 7
-O2357
-O3 -march=native246

Мусор не просто неправильный — он правдоподобный. Число, похожее на размер данных, легко принять за осмысленный ответ.

То же самое с типами аргументов. Функция add ждёт два int. Передадим два дробных:

var add = lib.get_function[c_int]("add")
print(add(Float64(2.0), Float64(3.0)))
1069267464

Снова ни ошибки, ни предупреждения.

Mojo сигнатуру не проверит — но проверить её можно, причём тем, кто её знает точно: компилятором C, по настоящему заголовочному файлу.

Пусть у библиотеки есть заголовок vec.h:

#include <stddef.h>
double dot(const double *a, const double *b, size_t n);
int add(int a, int b);

Рядом кладём маленький файл, который повторяет ту сигнатуру, что мы записали в Mojo:

#include "vec.h"
_Static_assert(__builtin_types_compatible_p(__typeof__(dot),
double(const double *, const double *, size_t)),
"dot: signature mismatch");
Окно терминала
gcc -fsyntax-only check_signatures.c

Совпало — тишина. Ошиблись, написав int вместо double, — сборка падает:

error: static assertion failed: "dot: signature mismatch"

Такой файл стоит держать рядом с обёрткой и прогонять в CI: если библиотека обновится и сигнатура изменится, узнаете сразу.

В главе Вызов Python из Mojo переход границы стоил около 300 наносекунд. Для C тот же замер — тысяча вызовов функции add из своей библиотеки — даёт:

КудаНаносекунд на вызов
функция из библиотеки на C≈ 2
функция на Python≈ 300

Разница — примерно в полтораста раз. Вызов C — это обычный машинный переход по адресу: ни упаковки в объекты, ни интерпретатора.

Поэтому правило «не зовите в горячем цикле» для C почти не действует: пару наносекунд на вызов можно себе позволить там, где триста — уже нельзя.

Пример использует только libc — она есть везде, собирать ничего не нужно. Каждый результат сверяется с Mojo или с заранее известным ответом.

libc_calls.mojo
# Вызовы стандартной библиотеки C из Mojo.
#
# Всё здесь — функции libc: она есть в любой системе, ставить ничего
# не нужно. Каждый результат сверяется с тем же расчётом на Mojo.
from std.ffi import external_call, c_int, c_double, c_size_t
from std.math import sqrt
def main():
print("1. числа: C и Mojo должны совпасть")
var x = c_double(2.0)
var from_c = external_call["sqrt", c_double](x)
print(" sqrt из C: ", from_c)
print(" sqrt из Mojo:", sqrt(Float64(2.0)))
print(" pow(2, 10): ", external_call["pow", c_double](c_double(2.0), c_double(10.0)))
print(" abs(-42): ", external_call["abs", c_int](c_int(-42)))
print("2. строка в C: считаются байты, а не символы")
var word = String("привет")
var c_length = external_call["strlen", c_size_t](word.as_c_string_span().ptr())
print(" strlen: ", c_length)
print(" byte_length: ", word.byte_length())
print("3. ловушка: срез без завершающего нуля")
var text = String("abcdef")
var view = text[byte=0:3]
print(" срез:", view, "| его длина:", view.byte_length())
# unsafe_ptr() среза указывает внутрь "abcdef": после "abc" нуля нет,
# и C читает дальше, пока не наткнётся на ноль исходной строки.
print(" strlen по unsafe_ptr среза:", external_call["strlen", c_size_t](view.unsafe_ptr()))
# Правильно: сделать из среза настоящую строку — у неё ноль на месте.
var copy = String(view)
print(" strlen по копии:", external_call["strlen", c_size_t](copy.as_c_string_span().ptr()))
Результат
1. числа: C и Mojo должны совпасть
   sqrt из C:    1.4142135623730951
   sqrt из Mojo: 1.4142135623730951
   pow(2, 10):   1024.0
   abs(-42):     42
2. строка в C: считаются байты, а не символы
   strlen:       12
   byte_length:  12
3. ловушка: срез без завершающего нуля
   срез: abc | его длина: 3
   strlen по unsafe_ptr среза: 6
   strlen по копии: 3

🎯 Проверь себя

Что указывается в квадратных скобках `external_call[...]`, а что — в круглых?

В квадратных — имя функции и тип результата: external_call["sqrt", c_double]. В круглых — аргументы; их типы компилятор выводит сам. Тип результата вывести нельзя: Mojo не видит исходник функции на C.

Почему `strlen` для строки «привет» возвращает 12, а не 6?

Потому что C считает байты, а не символы. В UTF-8 каждая кириллическая буква занимает два байта. Mojo говорит то же самое через byte_length().

Срез `text[byte=0:3]` от строки «abcdef» передали в `strlen` через `unsafe_ptr()` и получили 6. Почему?

Срез не копирует данные, а указывает внутрь исходной строки, и после abc нуля нет. strlen идёт по памяти до первого нуля — до конца abcdef. Правильно: превратить срез в строку String(view) и передать через as_c_string_span().ptr().

Функцию, возвращающую `double`, по ошибке объявили с `c_int`. Что скажет компилятор?

Ничего. Mojo не видит исходник на C и верит объявлению. Программа соберётся и вернёт мусор — в нашем замере 3 вместо 32.0: мы прочитали регистр rax, где лежал счётчик цикла функции. С другими флагами сборки число будет другим. Ловится это двумя способами: проверкой сигнатуры компилятором C по заголовку и прогоном на данных с известным ответом.

Как проверить, что сигнатура в Mojo совпадает с настоящей?

Компилятором C: в отдельном файле подключить настоящий заголовок и написать _Static_assert(__builtin_types_compatible_p(__typeof__(f), тип_из_Mojo), "..."). Команда gcc -fsyntax-only упадёт, если сигнатуры разошлись. Такую проверку стоит держать в CI.

Можно ли звать функцию на C внутри горячего цикла?

Как правило, да. Вызов стоит около двух наносекунд — это обычный машинный переход, без интерпретатора. Для сравнения, вызов Python стоит около трёхсот. Правило «не пересекать границу в цикле» относится к Python, а не к C.

Упаковка и распространение: как собрать программу на Mojo так, чтобы она запустилась на чужой машине — и что для этого нужно положить рядом.

Примеры проверены на Mojo 1.1.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0