Пишу, по большей части, про историю, свою жизнь и немного про программирование.

FFI: баг не будет исправлен?

В пхпешном FFI есть баг — можно, вызывав сишную функцию случайно изменить значение совершенно других переменных

Очень не хочется это признавать, но похоже тот баг, который я обнаружил в реализации FFI (интерфейса к языку Си) в ПХП, исправлен не будет. Собственно, на это мне давно указал один из читателей в комментариях к прошлой заметке на эту тему, но мне всё не верилось.

В тикете, который я завёл по проблеме, один из разработчиков языка написал, что придерживается мысли, что это преднамеренное проектное решение. Не думаю, что это действительно так, наверняка получилось случайно, но как всё поправить, не снизив существенно производительность модуля FFI, — неясно.

Видимо, придётся жить с тем, что есть.

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

Но раз это навсегда, давайте избегать сторонние эффекты правильно. Тот же читатель предлагает более корректный способ:

Первый параметр memcpy должен быть указателем на отдельно выделенную память достаточного размера. И в FFI есть метод для выделения памяти: FFI::new. Его и надо использовать вместо str_repeat.

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

function memcpy(?string &$dst, string $src):void
{
    $len = strlen($src);
    // изготавливаем тип, в котором уместится копируемая строка
    $type = FFI::arrayType(FFI::type('char'), [$len]);
    // выделяем место под изготовленный тип
    $destination = FFI::new($type);

    FFI::cdef('char *memcpy(char *dst, const char *src, size_t len);')
       ->memcpy($destination, $src, $len);
    // преобразуем область памяти в строку
    $dst = FFI::string($destination, $len);
}

$rock = "ROCK";
var_dump($rock); // «ROCK»
memcpy($rock, "SOCK");
var_dump($rock); // «SOCK»

Что тут происходит?

Мы хотим вызвать сишную функцию для копирования одной строки в другую. В месте, куда мы собираемся копировать, должно быть выделено достаточное количество памяти, мы её выделяем методом FFI::new, передавая ему на вход специально изготовленный тип — указатель на массив однобайтовых символов длиной c копируемую строку.

После копирования преобразуем область памяти в пхпешную строку. Память, выделенная при помощи FFI::new, освободится автоматически.

3 комментария
Мимо проходил 2020

По-хорошему, должно быть как-то так:

$type = FFI::arrayType(FFI::type(’char’), [$len + 1]);

Евгений Степанищев 2020

Зачем?

Мимо проходил 2020

А зачем выделять память под массив указателей на символ вместо массива символов?

Дополнительный элемент для null-terminated строк конкретно в этом случае не нужен, но если вдруг кто-то скопирует фрагмент кода, то могут возникнуть проблемы, если эта строка пойдёт в другие сишные функции.

Евгений Степанищев 2020

Вот зараза, правил-правил, в итоге сам себя запутал, да ещё и объяснение придумал, спасибо, что поправили!

Дополнительный элемент для null-terminated строк конкретно в этом случае не нужен, но если вдруг кто-то скопирует фрагмент кода, то могут возникнуть проблемы, если эта строка пойдёт в другие сишные функции.

Так я специально вызываю strncpy, чтобы этот бесполезный тут байт не выделять.

Мимо проходил 2020

strncpy() для этой задачи кстати не подходит, т. к. строки в PHP — binary-safe, а она работает с сишными:

$rock = «ROCK»;
var_dump($rock); // «ROCK»
strcpy($rock, «S\x00CK»);
var_dump($rock); // «S»

Евгений Степанищев 2020

Это всё-таки пример как бороться с багом, а не то как надо работать с пхпшными строками.

strcpy($rock, «S\x00CK»);

Тут, кстати, можно проще: «S\x00CK» → «S\0CK»

Добавлено позднее: А знаете, вы правы! Мне уже много раз указывали, что я могу вводить в заблуждение своими примерами недостаточно подготовленных людей, а мне всё кажется, что это не важно… Сейчас переделаю.

Спасибо вам большое!