> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP FFI

> 讲解 PHP FFI 的基础、可用环境，以及以 VOICEVOX Core for PHP 为例的实战包装模式。

## 前言

PHP FFI（Foreign Function Interface）是用于加载共享库、调用 C 函数、访问 C 数据结构的 PHP 扩展。无需自己编写 PHP 扩展，就能从 PHP 直接使用已有的 C API。

PHP 官方文档将 FFI 说明为一项低级且危险的功能。它是仅适合那些既理解 C 又理解目标库 API 的开发者使用的功能。

<Warning>
  FFI 不是被安全抽象过的普通 PHP API。误用指针、所有权、释放函数会导致崩溃或内存破坏。
</Warning>

FFI 也不是「让程序更快的功能」。PHP 官方文档同样指出，FFI 的数据结构访问比原生 PHP 数组或对象更慢。选择 FFI 的理由并非速度，而是希望从 PHP 使用已有的 C 库。

## FFI 可用的环境与限制

PHP 官方文档中，启用 FFI 扩展的方法如下。

* 编译 PHP 时带上 `--with-ffi`
* 在 Windows 上，通过 `php.ini` 启用 `php_ffi.dll`
* 通过 `php.ini` 的 `ffi.enable` 控制是否允许使用

`ffi.enable` 有以下 3 个值：

| 设置值       | 含义                             |
| --------- | ------------------------------ |
| `true`    | 启用 FFI API                     |
| `false`   | 禁用 FFI API                     |
| `preload` | 只允许 CLI SAPI 与预加载文件中使用 FFI API |

<Info>
  在 Web 服务器环境中，通常会设置 `ffi.enable=false` 或 `ffi.enable=preload`。包括 Laravel Cloud 在内的托管环境，也不能保证在常规 Web 应用中都能使用 FFI。
</Info>

因此，即使要在 Laravel 应用中使用 FFI，也应优先考虑 CLI 工具或批处理场景。以 FPM 或 Apache mod\_php 常态启用为前提并不安全。

## 基本用法

PHP FFI 的基本 API 有 `FFI::cdef()`、`FFI::new()`、`FFI::addr()`、`FFI::string()` 四个。

```php theme={null}
<?php

$ffi = FFI::cdef(<<<'CDEF'
typedef unsigned int time_t;
typedef unsigned int suseconds_t;

struct timeval {
    time_t tv_sec;
    suseconds_t tv_usec;
};

struct timezone {
    int tz_minuteswest;
    int tz_dsttime;
};

int gettimeofday(struct timeval *tv, struct timezone *tz);
CDEF, 'libc.so.6');

$tv = $ffi->new('struct timeval');
$tz = $ffi->new('struct timezone');

$ffi->gettimeofday(FFI::addr($tv), FFI::addr($tz));

echo $tv->tv_sec.PHP_EOL;
```

这个例子中要点如下：

* `FFI::cdef()` 从 C 声明字符串和共享库名创建 FFI 对象
* `$ffi->new('struct timeval')` 分配 C 数据结构
* `FFI::addr($tv)` 用来构造 `struct timeval *` 这样的指针参数
* 可以像 `$tv->tv_sec` 一样访问结构体字段

对于接收字符串或二进制的 API，使用 `FFI::string()`。

```php theme={null}
<?php

$jsonPtr = $ffi->new('char*');

// 假设 C 侧向 $jsonPtr 写入结果指针
$result = $ffi->some_function(FFI::addr($jsonPtr));

if ($result !== 0) {
    throw new RuntimeException('C API call failed.');
}

$json = FFI::string($jsonPtr);
```

<Tip>
  由 `FFI::new()` 分配的内存通常会通过 PHP 的引用计数释放。而 C 库返回的指针则可能需要该库专用的 `free` 函数释放。请针对每个 API 明确所有权归属。
</Tip>

## 加载头文件

使用 `FFI::load()` 可以从 C 头文件读入声明。头文件中可以写入 `FFI_SCOPE` 与 `FFI_LIB`。

```c theme={null}
#define FFI_SCOPE "mylib"
#define FFI_LIB "/absolute/path/to/libmylib.so"

typedef struct MyContext MyContext;

MyContext *mylib_new(void);
void mylib_delete(MyContext *context);
```

```php theme={null}
<?php

FFI::load(__DIR__.'/mylib.h');

$ffi = FFI::scope('mylib');
$context = $ffi->mylib_new();
```

不过，PHP 官方文档说明，无论是 `FFI::cdef()` 还是 `FFI::load()`，都不能使用普通的 C 预处理指令。`#include`、`#define`、条件编译无法直接传入。

因此，实际项目中经常选择以下两种方式之一：

<Steps>
  <Step title="简单的 API 直接写在 `FFI::cdef()` 中">
    若依赖的类型和函数很少，只在 PHP 侧嵌入必要的声明。
  </Step>

  <Step title="复杂 API 则准备一份预处理过的头文件">
    从原始头文件中剥离宏与条件编译，另存为专供 FFI 使用的头文件。
  </Step>
</Steps>

## 实际用例：VOICEVOX Core for PHP

[VOICEVOX Core for PHP](/zh/packages/voicevox-core-php) 是从 Pure PHP 使用 VOICEVOX CORE 的 C 动态库的实例。这里汇集了 FFI 的实用模式，比抽象说明更容易理解。

### 1. 持有为 FFI 整理过的头文件

原始的 VOICEVOX Core 头文件不会直接使用，而是把面向 FFI 的声明抽取到 `headers/voicevox_core_ffi.h`。这里明确了不透明指针、结构体与 free 函数。

```c theme={null}
typedef struct VoicevoxSynthesizer VoicevoxSynthesizer;

typedef struct VoicevoxInitializeOptions {
    int32_t  acceleration_mode;
    uint16_t cpu_num_threads;
} VoicevoxInitializeOptions;

int32_t voicevox_synthesizer_new(
    const struct VoicevoxOnnxruntime *onnxruntime,
    const struct OpenJtalkRc *open_jtalk,
    struct VoicevoxInitializeOptions options,
    struct VoicevoxSynthesizer **out_synthesizer
);

void voicevox_synthesizer_delete(struct VoicevoxSynthesizer *synthesizer);
void voicevox_json_free(char *json);
void voicevox_wav_free(uint8_t *wav);
```

这种设计的关键在于：不向 PHP 侧暴露 C 的细节内部结构，而是收敛到不透明句柄与函数调用。PHP 侧要处理的面积变小，包装类的组织也更清晰。

### 2. 将 `FFI::cdef()` 集中在一处

`src/VoicevoxFFI.php` 将头文件读取与库路径解析集中在一处。

```php theme={null}
<?php

class VoicevoxFFI
{
    private static ?FFI $ffi = null;

    public static function getInstance(): FFI
    {
        return self::$ffi ??= FFI::cdef(
            file_get_contents(__DIR__.'/../headers/voicevox_core_ffi.h'),
            self::getLibraryPath(),
        );
    }

    public static function getLibraryPath(): string
    {
        if ($path = getenv('VOICEVOX_CORE_LIB_PATH')) {
            return $path;
        }

        return match (PHP_OS_FAMILY) {
            'Darwin' => 'libvoicevox_core.dylib',
            'Windows' => 'voicevox_core.dll',
            default => 'libvoicevox_core.so',
        };
    }
}
```

这样一来，你的应用侧就不必直接调用 `FFI::cdef()`。库路径的差异也可以通过环境变量与 OS 判断来吸收。

### 3. 用对象包装 out parameter

`src/Synthesizer.php` 的构造函数分配 `struct VoicevoxSynthesizer*`，并将其地址传给 `voicevox_synthesizer_new()`。

```php theme={null}
<?php

$options = $this->ffi->voicevox_make_default_initialize_options();
$options->acceleration_mode = $accelerationMode->value;
$options->cpu_num_threads = $cpuNumThreads;

$ptr = $this->ffi->new('struct VoicevoxSynthesizer*');

$result = $this->ffi->voicevox_synthesizer_new(
    $onnxruntime->handle(),
    $openJtalk->handle(),
    $options,
    FFI::addr($ptr),
);

$this->handle = $ptr;
```

这是在 PHP 中接收 C API 中 `Type **out_value` 的基本模式：

* 用 `new('struct VoicevoxSynthesizer*')` 创建指针变量
* 用 `FFI::addr($ptr)` 传入 `Type **`
* 将得到的句柄保存到 PHP 对象的属性中

### 4. C 返回的内存拷贝到 PHP 字符串后立即释放

VOICEVOX Core for PHP 在收到 JSON 字符串或 WAV 二进制时，会先用 `FFI::string()` 拷贝为 PHP 字符串，然后再调用 C 侧的 free 函数。

```php theme={null}
<?php

$jsonPtr = $this->ffi->voicevox_synthesizer_create_metas_json($this->handle);

$json = FFI::string($jsonPtr);
$this->ffi->voicevox_json_free($jsonPtr);

return $json;
```

```php theme={null}
<?php

$wavSize = $this->ffi->new('uint64_t');
$wavPtr = $this->ffi->new('uint8_t*');

$result = $this->ffi->voicevox_synthesizer_tts(
    $this->handle,
    $text,
    $styleId,
    $options,
    FFI::addr($wavSize),
    FFI::addr($wavPtr),
);

$wav = FFI::string($wavPtr, (int) $wavSize->cdata);
$this->ffi->voicevox_wav_free($wavPtr);

return $wav;
```

这种「拷贝后立即释放」的流程是 FFI 中最重要的实现模式之一。不将 C 侧缓冲区混入 PHP 的生命周期，能极大简化所有权管理。

## 注意事项与最佳实践

| 视角    | 实践要点                                   |
| ----- | -------------------------------------- |
| 运行环境  | 优先按 CLI 场景设计                           |
| 头文件管理 | 不直接使用原头文件，而是另行管理面向 FFI 的声明             |
| 库路径   | 可通过绝对路径或环境变量切换                         |
| 内存管理  | 使用 `FFI::string()` 拷贝到 PHP，务必调用库专用释放函数 |
| 包装设计  | 不要把原生 `CData` 扩散到整个应用，收敛到专用类中          |
| 兼容性   | 一起确认 PHP 版本与目标库的 ABI 变更                |

<Warning>
  将 FFI 直接混入 Laravel 应用的请求/响应循环，会让环境差异与故障定位骤然变难。请先在 CLI 命令或队列外的一次性进程中让它跑通。
</Warning>

FFI 适用的场景是：已有稳定的 C API，但又不至于要专门写 PHP 扩展。反过来，若想在通用 Web 托管环境中运行，或者可以完全用 PHP 完成的处理，就不适合使用 FFI。

## 小结

使用 PHP FFI，你可以从 Pure PHP 调用已有的 C 库。但 FFI 是一项低级且危险的机制，也未必在 Web 服务器上可用。

从 VOICEVOX Core for PHP 的实现可以看到，实践中要点有 4 条：

1. 为 FFI 单独准备头文件
2. 将 `FFI::cdef()` 与库路径解析集中在一处
3. 用专用类包装不透明指针
4. C 返回的内存先拷贝到 PHP，再用专用函数释放

结合 VOICEVOX Core for PHP 的页面一起阅读，可以在 FFI 的概念与实际包设计之间来回印证，加深理解。


## Related topics

- [VOICEVOX Core for PHP](/zh/packages/voicevox-core-php/index.md)
- [安装与配置 - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/installation.md)
- [原生模式：Talk（文本语音合成） - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/native-talk.md)
- [原生模式：Song（歌声合成） - VOICEVOX for Laravel](/zh/packages/laravel-voicevox/native-song.md)
- [VOICEVOX for Laravel](/zh/packages/laravel-voicevox/index.md)
