시작하며
PHP FFI(Foreign Function Interface)는, 공유 라이브러리를 읽어들이고, C 함수를 호출하며, C의 데이터 구조에 접근하기 위한 PHP 확장입니다. PHP 확장을 직접 만들지 않아도, 기존의 C API를 PHP에서 직접 사용할 수 있습니다. PHP 공식 문서는, FFI를 저레벨이고 위험한 기능으로 설명하고 있습니다. C와 대상 라이브러리의 API를 이해한 개발자만이 사용해야 하는 기능입니다. 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가지 값을 취합니다.
Web 서버 환경에서는
ffi.enable=false나 ffi.enable=preload가 자주 사용됩니다. Laravel Cloud를 포함한 호스팅 환경에서도, 일반적인 Web 앱으로서 FFI를 사용할 수 있다고 한정할 수 없습니다.기본적인 사용법
PHP FFI의 기본은FFI::cdef(), FFI::new(), FFI::addr(), FFI::string()의 4가지입니다.
FFI::cdef()는 C의 선언 문자열과 공유 라이브러리명에서 FFI 오브젝트를 만든다$ffi->new('struct timeval')은 C의 데이터 구조를 확보한다FFI::addr($tv)는struct timeval *와 같은 포인터 인수를 만든다$tv->tv_sec와 같이 구조체 필드에 접근할 수 있다
FFI::string()을 사용합니다.
헤더 파일의 로딩
FFI::load()를 사용하면, C 헤더 파일에서 선언을 로드할 수 있습니다. 헤더 파일에는 FFI_SCOPE와 FFI_LIB를 쓸 수 있습니다.
FFI::cdef()와 FFI::load()의 어느 쪽에서도 일반적인 C 전처리기 명령은 사용할 수 없다고 설명하고 있습니다. #include, #define, 조건부 컴파일을 그대로 전달할 수 없습니다.
그 때문에, 실운용에서는 다음의 어느 쪽인가를 선택하는 경우가 많습니다.
1
단순한 API라면 `FFI::cdef()`에 직접 쓴다
의존하는 타입이나 함수가 적다면, 필요한 선언만을 PHP 측에 삽입합니다.
2
복잡한 API라면 전처리된 헤더를 준비한다
원래 헤더에서 매크로나 조건부 컴파일을 제거한, FFI용 헤더를 별도의 파일로 관리합니다.
실제 유스케이스: VOICEVOX Core for PHP
VOICEVOX Core for PHP는, VOICEVOX CORE의 C 동적 라이브러리를 Pure PHP에서 사용하는 실제 예입니다. FFI의 실용 패턴이 정리되어 있어, 추상적인 설명보다 이해하기 쉬운 소재입니다.1. FFI용으로 정형한 헤더를 갖는다
VOICEVOX Core의 원래 헤더는 그대로는 사용하지 않고,headers/voicevox_core_ffi.h에 FFI용의 선언을 잘라내고 있습니다. 여기에서는 불투명 포인터, 구조체, free 함수를 명시하고 있습니다.
2. FFI::cdef()를 한 곳에 집약한다
src/VoicevoxFFI.php는, 헤더 로딩과 라이브러리 경로 해결을 한 곳에 집약하고 있습니다.
FFI::cdef()를 직접 호출하지 않아도 됩니다. 라이브러리 경로의 차분도, 환경 변수와 OS 판정으로 흡수할 수 있습니다.
3. out parameter를 래핑하여 오브젝트화한다
src/Synthesizer.php의 컨스트럭터는, struct VoicevoxSynthesizer*를 확보하고, voicevox_synthesizer_new()에 그 주소를 전달하고 있습니다.
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 함수를 호출하고 있습니다.
주의점과 베스트 프랙티스
FFI가 적합한 것은, 이미 안정된 C API가 있고, PHP 확장을 직접 만들 정도는 아닌 케이스입니다. 반대로, 일반적인 Web 호스팅에서 동작시키고 싶은 기능이나, PHP만으로 완결할 수 있는 처리에는 적합하지 않습니다.
정리
PHP FFI를 사용하면, 여러분은 기존의 C 라이브러리를 Pure PHP에서 호출할 수 있습니다. 단, FFI는 저레벨이고 위험한 구조입니다. Web 서버에서 항상 사용할 수 있다고 한정할 수도 없습니다. VOICEVOX Core for PHP의 구현을 보면, 실전에서는 다음 4점이 중요하다는 것을 알 수 있습니다.- FFI용의 헤더를 별도로 준비한다
FFI::cdef()와 라이브러리 경로 해결을 한 곳에 집약한다- 불투명 포인터를 전용 클래스에서 래핑한다
- C가 반환한 메모리는 PHP에 복사한 뒤 전용 함수로 해방한다