pnl は PHP Native Library の略で,SDL や libusb などを使ったネイティブアプリケーションを PHP で構築する際に必要となるライブラリを簡易的に操作,管理するためのパッケージマネージャです。
Composer にネイティブ動作用のランタイマーが付属しているものとイメージするとわかりやすいかもしれません。
ランタイマー自体は PHP で書かれていますが,パッケージマネージャ自体は Rust で実装しているので,高速です。
内部で時折登場する pnlx は PHP Native Library eXtensions/eXecuter の略です。パッケージマネージャとビルダー/ランタイマーを棲み分けしています。
またオフィシャルっていうほどのものではないのですが,デフォルトリポジトリである程度のパッケージをカバーしています。適宜追加しています。(足りなければコントリビューションお待ちしてます)
PHP FFI 使用までのステップを簡素化
SDL や libusb をインストールできたとて,PHP FFI でそれらを使用するには,一癖あります。
各ライブラリが提供している extern された情報(大半は .h にかかれている内容ですが)をもとに FFI::cdef を組み立てなければいけません。
PHP の FFI はマクロが動作しないので, #define が書かれていたりすると FFI が動作しないので取り除いたり,手を加えなければいけないケースがあります。
ライブラリも,OS・ディストリ によってインストール先が微妙に異なるケースもあるので,これらを管理するのもひと手間です。
そこで,pnl は OS・ディストリ依存を極力少なくしつつ,FFI::cdef のことを気にせずネイティブライブラリを簡易に扱えるようにしました。
ただ単純に:
pnl install libsdl
するだけで,SDL を使うのに必要なライブラリ,PHP のランタイマまで生成します。以下は macOS 上の例:
pnl install libsdl
› resolving libsdl from https://github.com/m3m0r7/pnl-packages/tree/main/packages
libsdl/libsdl declares install scripts but does not carry install_script_hash 788f9629abde05a99e287995aa559d96090be01abbb348b76c9b51bb740d56fd
Install scripts can execute arbitrary commands. Continue installing libsdl/libsdl? [y/N] y
/opt/homebrew/Cellar/sdl2/2.32.10/bin/sdl2-config
/opt/homebrew/Cellar/sdl2/2.32.10/include/SDL2/ (78 files)
/opt/homebrew/Cellar/sdl2/2.32.10/lib/libSDL2-2.0.0.dylib
/opt/homebrew/Cellar/sdl2/2.32.10/lib/cmake/ (2 files)
/opt/homebrew/Cellar/sdl2/2.32.10/lib/pkgconfig/sdl2.pc
/opt/homebrew/Cellar/sdl2/2.32.10/lib/ (4 other files)
/opt/homebrew/Cellar/sdl2/2.32.10/sbom.spdx.json
/opt/homebrew/Cellar/sdl2/2.32.10/share/aclocal/sdl2.m4
/opt/homebrew/Cellar/sdl2_image/2.8.8/include/SDL2/SDL_image.h
/opt/homebrew/Cellar/sdl2_image/2.8.8/lib/libSDL2_image-2.0.0.dylib
/opt/homebrew/Cellar/sdl2_image/2.8.8/lib/cmake/ (4 files)
/opt/homebrew/Cellar/sdl2_image/2.8.8/lib/pkgconfig/SDL2_image.pc
/opt/homebrew/Cellar/sdl2_image/2.8.8/lib/ (2 other files)
/opt/homebrew/Cellar/sdl2_image/2.8.8/sbom.spdx.json
/opt/homebrew/Cellar/sdl2_ttf/2.24.0/include/SDL2/SDL_ttf.h
/opt/homebrew/Cellar/sdl2_ttf/2.24.0/lib/libSDL2_ttf-2.0.0.dylib
/opt/homebrew/Cellar/sdl2_ttf/2.24.0/lib/cmake/ (2 files)
/opt/homebrew/Cellar/sdl2_ttf/2.24.0/lib/pkgconfig/SDL2_ttf.pc
/opt/homebrew/Cellar/sdl2_ttf/2.24.0/lib/ (2 other files)
/opt/homebrew/Cellar/sdl2_ttf/2.24.0/sbom.spdx.json
• libsdl/libsdl native dependencies already present
✓ resolved sdl2 2.32.10 libSDL2.dylib
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/libsdl.ffi.php
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/Libsdl.php
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/LibsdlContext.php
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/index.php
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/functions.php
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/function.aliases.php
✓ generated ./@pnlx/packages/libsdl/libsdl/2.32.10/src/generated/libsdl.bridge.rs
✓ installed extension libsdl/libsdl
› usage — libsdl/libsdl (EXAMPLES.md)
libsdl/libsdl examples
╭┄┄┄ php
┊ use Pnlx\Libsdl\Libsdl;
┊ use Pnlx\Runtime;
┊ use function Pnlx\Util\is_null;
┊
┊ const SDL_INIT_VIDEO = 0x00000020;
┊ const SDL_WINDOWPOS_CENTERED = 0x2FFF0000;
┊ const SDL_WINDOW_SHOWN = 0x00000004;
┊
┊ $runtime = new Runtime(__DIR__);
┊ $sdl = $runtime->load(Libsdl::class);
┊
┊ $sdl->SDL_Init(SDL_INIT_VIDEO);
┊ $window = $sdl->SDL_CreateWindow('Hello', SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 360, SDL_WINDOW_SHOWN);
┊ if (is_null($window)) {
┊ throw new RuntimeException('SDL_CreateWindow failed: ' . $sdl->SDL_GetError());
┊ }
┊ $sdl->SDL_Delay(2000);
┊ $sdl->SDL_DestroyWindow($window);
┊ $sdl->SDL_Quit();
╰┄┄┄
added 1 extension(s) in 6.45s
あとは PHP で以下のように呼び出すだけで,Hello World! が出力できます。
<?php
declare(strict_types=1);
chdir(__DIR__);
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/@pnlx/autoload.php';
use Pnlx\Libsdl\Libsdl;
use Pnlx\Runtime;
use function Pnlx\Util\is_null;
const SDL_INIT_VIDEO = 0x00000020;
const SDL_WINDOWPOS_CENTERED = 0x2FFF0000;
const SDL_WINDOW_SHOWN = 0x00000004;
$runtime = new Runtime(__DIR__);
/** @var Libsdl $sdl */
$sdl = $runtime->load(Libsdl::class);
// A tiny 5x7 bitmap font for the characters in "Hello World!".
// '1' marks a lit pixel; rows are top-to-bottom.
$font = [
'H' => ['10001', '10001', '10001', '11111', '10001', '10001', '10001'],
'e' => ['00000', '00000', '01110', '10001', '11111', '10000', '01110'],
'l' => ['01100', '00100', '00100', '00100', '00100', '00100', '01110'],
'o' => ['00000', '00000', '01110', '10001', '10001', '10001', '01110'],
'W' => ['10001', '10001', '10001', '10101', '10101', '11011', '10001'],
'r' => ['00000', '00000', '10110', '11001', '10000', '10000', '10000'],
'd' => ['00001', '00001', '01101', '10011', '10001', '10001', '01111'],
'!' => ['00100', '00100', '00100', '00100', '00100', '00000', '00100'],
' ' => ['00000', '00000', '00000', '00000', '00000', '00000', '00000'],
];
$window = null;
$renderer = null;
$initialized = false;
try {
$result = $sdl->SDL_Init(SDL_INIT_VIDEO);
if ($result !== 0) {
throw new RuntimeException('SDL_Init failed: ' . $sdl->SDL_GetError());
}
$initialized = true;
$window = $sdl->SDL_CreateWindow(
'Hello World!',
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
640,
360,
SDL_WINDOW_SHOWN
);
if (is_null($window)) {
throw new RuntimeException('SDL_CreateWindow failed: ' . $sdl->SDL_GetError());
}
$renderer = $sdl->SDL_CreateRenderer($window, -1, 0);
if (is_null($renderer)) {
throw new RuntimeException('SDL_CreateRenderer failed: ' . $sdl->SDL_GetError());
}
// Clear to a dark background.
$sdl->SDL_SetRenderDrawColor($renderer, 0x1E, 0x1E, 0x1E, 0xFF);
$sdl->SDL_RenderClear($renderer);
// Draw "Hello World!" in the window, scaling each font pixel into a block.
// SDL_RenderDrawPoint takes only integers, so no FFI structs are needed.
$sdl->SDL_SetRenderDrawColor($renderer, 0xFF, 0xFF, 0xFF, 0xFF);
$scale = 6;
$x = 70;
$y = 150;
foreach (str_split('Hello World!') as $char) {
$glyph = $font[$char] ?? $font[' '];
foreach ($glyph as $row => $bits) {
for ($col = 0; $col < 5; $col++) {
if ($bits[$col] !== '1') {
continue;
}
for ($dy = 0; $dy < $scale; $dy++) {
for ($dx = 0; $dx < $scale; $dx++) {
$sdl->SDL_RenderDrawPoint($renderer, $x + $col * $scale + $dx, $y + $row * $scale + $dy);
}
}
}
}
$x += 6 * $scale; // 5px glyph + 1px gap
}
$sdl->SDL_RenderPresent($renderer);
$until = microtime(true) + 3.0;
while (microtime(true) < $until) {
$sdl->SDL_PumpEvents();
$sdl->SDL_Delay(16);
}
} finally {
if (!is_null($renderer)) {
$sdl->SDL_DestroyRenderer($renderer);
}
if (!is_null($window)) {
$sdl->SDL_DestroyWindow($window);
}
if ($initialized) {
$sdl->SDL_Quit();
}
}

また, use_functions を有効にすることで,C ライクな書きぶりもできるようになります。
$result = SDL_Init(SDL_INIT_VIDEO);
$window = SDL_CreateWindow(
'Hello World!',
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
640,
360,
SDL_WINDOW_SHOWN
);
$renderer = SDL_CreateRenderer($window, -1, 0);
if (is_null($renderer)) {
throw new RuntimeException('SDL_CreateRenderer failed: ' . SDL_GetError());
}
// Clear to a dark background.
SDL_SetRenderDrawColor($renderer, 0x1E, 0x1E, 0x1E, 0xFF);
SDL_RenderClear($renderer);
// Draw "Hello World!" in the window, scaling each font pixel into a block.
// SDL_RenderDrawPoint takes only integers, so no FFI structs are needed.
SDL_SetRenderDrawColor($renderer, 0xFF, 0xFF, 0xFF, 0xFF);
// ...
pnl の仕組み
pnl は直接ライブラリを呼び出すのではなく,Rust で実装されたブリッジを介します。ゆえに pnl を使うには Rust 環境が必須になります。(例: https://github.com/m3m0r7/pnl/blob/main/tests/golden/example/example.bridge.rs )
PHP FFI <--> Rust FFI Bridge <--> 各ライブラリ
現行バージョンではブリッジを入れているメリットは多くないのですが,将来的には,PHP FFI で呼び出せないケースが現れた場合に当該部分を再実装できるようにしたり,複数ライブラリを 1 枚にまとめられる機能,補助的な実装を埋め込めるように想定しています(近々実装します)。
pnl 自体のインストール方法は Composer を介すまたは,リリースからバイナリを落としてくる,マニュアルインストールの 3 パターンです。
PHPer の皆さまであれば,Composer を使うのが一番手っ取り早いと思います。
composer install m3m0r7/pnl
とすると, vendor/bin に pnl が入るので:
vendor/bin/pnl install libsdl
のようにすることでネイティブライブラリをインストールすることができます。
マニュアルインストールはリポジトリをクローンしてきて:
make build && sudo make install
するだけです。