> ## 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 확장을 직접 만들지 않아도, 기존의 C API를 PHP에서 직접 사용할 수 있습니다.

PHP 공식 문서는, FFI를 저레벨이고 위험한 기능으로 설명하고 있습니다. C와 대상 라이브러리의 API를 이해한 개발자만이 사용해야 하는 기능입니다.

<Warning>
  FFI는 안전하게 추상화된 일반적인 PHP API가 아닙니다. 포인터, 소유권, 해방 함수를 잘못하면 크래시나 메모리 파괴로 이어집니다.
</Warning>

FFI는 "속도를 위한 기능"도 아닙니다. PHP 공식 문서에서도, FFI의 데이터 구조 접근은 네이티브한 PHP 배열이나 오브젝트보다 느리다고 설명하고 있습니다. FFI를 선택하는 이유는 속도가 아니라, 기존의 C 라이브러리를 PHP에서 사용하고 싶기 때문입니다.

## 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와 preload된 파일에서만 FFI API를 허가 |

<Info>
  Web 서버 환경에서는 `ffi.enable=false`나 `ffi.enable=preload`가 자주 사용됩니다. Laravel Cloud를 포함한 호스팅 환경에서도, 일반적인 Web 앱으로서 FFI를 사용할 수 있다고 한정할 수 없습니다.
</Info>

그러므로, Laravel 앱에서 FFI를 사용하는 경우에도, 우선은 CLI 도구나 배치 처리에서 성립하는지를 생각해 주세요. FPM이나 Apache mod\_php에서 상시 활성화한다는 전제는 피하는 편이 안전합니다.

## 기본적인 사용법

PHP FFI의 기본은 `FFI::cdef()`, `FFI::new()`, `FFI::addr()`, `FFI::string()`의 4가지입니다.

```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](/ko/packages/voicevox-core-php)는, VOICEVOX CORE의 C 동적 라이브러리를 Pure PHP에서 사용하는 실제 예입니다. FFI의 실용 패턴이 정리되어 있어, 추상적인 설명보다 이해하기 쉬운 소재입니다.

### 1. FFI용으로 정형한 헤더를 갖는다

VOICEVOX Core의 원래 헤더는 그대로는 사용하지 않고, `headers/voicevox_core_ffi.h`에 FFI용의 선언을 잘라내고 있습니다. 여기에서는 불투명 포인터, 구조체, 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);
```

이 설계에서 중요한 것은, C의 상세한 내부 구조를 PHP 측에 보이지 않고, 불투명 핸들과 함수 호출에 가두고 있다는 것입니다. 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;
```

이 패턴은, C API의 `Type **out_value`를 PHP에서 받는 기본 형태입니다.

* `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>
  Laravel 앱의 request/response 사이클에 FFI를 직접 섞으면, 환경 차분과 장애 분리가 갑자기 어려워집니다. 우선은 CLI 커맨드나 큐 이외의 단발 프로세스에서 성립시켜 주세요.
</Warning>

FFI가 적합한 것은, 이미 안정된 C API가 있고, PHP 확장을 직접 만들 정도는 아닌 케이스입니다. 반대로, 일반적인 Web 호스팅에서 동작시키고 싶은 기능이나, PHP만으로 완결할 수 있는 처리에는 적합하지 않습니다.

## 정리

PHP FFI를 사용하면, 여러분은 기존의 C 라이브러리를 Pure PHP에서 호출할 수 있습니다. 단, 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](/ko/packages/voicevox-core-php/index.md)
- [설치와 설정 - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/installation.md)
- [네이티브 모드: 토크 (텍스트 음성 합성) - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/native-talk.md)
- [네이티브 모드: 송 (노래 음성 합성) - VOICEVOX for Laravel](/ko/packages/laravel-voicevox/native-song.md)
- [VOICEVOX for Laravel](/ko/packages/laravel-voicevox/index.md)
