Share
Embed
ファミコンでPHPを動かす / PHP on the Famicom side b
PHPカンファレンス愛媛2026の資料です。
以前、PHPでファミコンエミュレータを作りました。ファミコンの仕様全体を理解しながらPHPでそれを実装する体験は今までのプログラマ人生で初の経験でも特別で、とても楽しいものでした。
PHPでファミコンを動かしたら、こんどはファミコンでPHP動かしたくなりました。
ということで、ファミコンで動くPHPコンパイラとPHP VMを実装してみました。
ファミコンは6502派生の8bit CPUを持ち、64KBのメモリ空間にアクセスできますが、ゲーム機として設計されているのでゲームプログラムや画面に表示されるキャラクタはカートリッジROMに格納されるの前提であり、RAMは2KBしか搭載していません。
このトークではファミコンでPHPを動かすために必要な設計、実装方法、実機でPHPを動かすためのノウハウなどを実機デモを含めてお話します。
Write once, run anywhere.
PHPをあらゆる環境で動かしましょう!
HASEGAWA Tomoki
October 02, 2026
Transcript
62 slides
-
𝕏 はせがわ ともき 長谷川 智希 @tomzoh 所属 http://www.dgcircus.com デジタルサーカス株式会社 来たれ!PHPer!We
are hiring! 寄稿・著書 技術カンファレンス主宰 -
𝕏 はせがわ ともき 長谷川 智希 @tomzoh ライフワーク 技術カンファレンス運営/参加, 各種プログラム開発, ,
CPU, レトロゲーム機, 電子工作, ビール, サッカー観戦, レンタルカートレース, キャンピングカー, 宝塚歌劇… -
どういうこと…? • 2018年にPHPでファミコンエミュレータを書いた • つまり、PHPでファミコンを動かした • 逆もやってみたい • ファミコンでPHPを動かす •
ファミコンは正式名称 ファミリーコンピュータ • つまりコンピュータでありプログラムを動かせる • nesphp: ファミコン(海外での名前: NES)で動くPHP ファミコンでPHPを動かす 長谷川智希 @tomzoh 6 -
そもそもPHPとは…? • PHPのソースコードをダウンロードして ./con gure → make とかやって出 てきたモノは間違いなくPHPであろう •
macOS版 php 実行ファイルは20MB • 我々が持っているのは32KBのROMと2KBのRAM • 単純比較はできないけど、まあ普通に考えてファミコンには載せられない • PHPすべての機能をサポートしなくても良かろう • str_rot13() とか… • 本気の実用じゃないし、最低限の機能を実装してみるか… 長谷川智希 @tomzoh fi ファミコンでPHPを動かす 10 -
PHPらしさ • ただ、そうは言っても、「これはPHPです」と言える感じにしたい • PHPらしさ: • PHPソース → opcode →
VMで実行の流れ • opcodeの構造がPHPと同じ • 変数やリテラルがzvalで表現される • zval: PHPのソースに出てくるCの構造体 ファミコンでPHPを動かす 長谷川智希 @tomzoh 11 -
opcode • PHPはソースコードをopcodeにコンパイルし <?php for ($i = 0; $i <
10; $i++){ echo($i.PHP_EOL); } opcodeをPHP VMで実行する • opcacheはコンパイル済のopcodeを キャッシュしておいてコンパイルの時間を 節約する仕組み ファミコンでPHPを動かす 長谷川智希 @tomzoh 0000 ASSIGN CV0($i) int(0) 0001 JMP 0005 0002 T2 = CONCAT CV0($i) string(" ") 0003 ECHO T2 0004 PRE_INC CV0($i) 0005 T4 = IS_SMALLER CV0($i) int(10) 0006 JMPNZ T4 0002 12 -
つくってみた nesphpの組込関数 分類 画面(BG) • Claude Codeと相談しながら開発 • コンパイラ/VMのプログラムサイズを見ながら スプライト
徐々に実装 • 変数・配列代入, 各種演算, 比較, while, if, for(;;), 組込関数, … • できるだけシンプルに… • 1パスコンパイラ • ソースを先頭から1回だけ走査する • function(), break/continue非対応 ファミコンでPHPを動かす 長谷川智希 @tomzoh 同期・入力 関数 nes_put() nes_puts() nes_putint() nes_cls() nes_attr() nes_palette() nes_bg_color() nes_chr_bg() nes_sprite_at() nes_sprite_attr() nes_chr_spr() nes_vsync() nes_btn() fgets(STDIN) nes_srand() nes_rand() nes_peek() nes_poke() nes_peek16() nes_pokestr() 乱数 メモリ user RAM 256B メモリ nes_peek_ext() ext RAM 8KB nes_poke_ext() nes_peek16_ext() nes_pokestr_ext() 配列 count() 13 -
nesphp独自命令 • ファミコンは8x8のパターンを定義して画面に敷き詰める設計 • 「文字」という概念はなく、パターン番号何番をどこに置くか • 8x8の単位でしか配置できない • BG面と呼ぶ •
マリオとか敵キャラとかはスプライトという機能を使って表示する • スプライトは1ピクセル単位で好きな場所に配置できる • そのかわり横に並べられる数や画面内に表示できる数に制限がある • 制限を超えると表示されないので意図的に交互に消す = キャラのチラツキ • 「BG+スプライト」 • 初代プレステより前のゲーム機はだいたいこの設計 • echo("HELLO, WORLD"); するにしても「どこに」を指定する機能が必要 • nesphp独自命令が必要 ファミコンでPHPを動かす 長谷川智希 @tomzoh 14 -
独自命令の実装 nesphpの組込関数 分類 画面(BG) • ファミコン特有の機能を実装するために nes_*() が大量にある スプライト •
Zend VMのopcodeは200個弱あり、番号が ついている 同期・入力 • ZEND_ECHO=136, ZEND_ASSIGN_DIM=147 など • opcodeは1byteで $D2〜$FF が空いている • 空きエリアに独自命令を割り当てている • ソースコードをnesphpコンパイラがパースする 時に独自命令の番号を持ったopcodeにコンパ イルしている ファミコンでPHPを動かす 長谷川智希 @tomzoh 関数 nes_put() nes_puts() nes_putint() nes_cls() nes_attr() nes_palette() nes_bg_color() nes_chr_bg() nes_sprite_at() nes_sprite_attr() nes_chr_spr() nes_vsync() nes_btn() fgets(STDIN) nes_srand() nes_rand() nes_peek() nes_poke() nes_peek16() nes_pokestr() 乱数 メモリ user RAM 256B メモリ nes_peek_ext() ext RAM 8KB nes_poke_ext() nes_peek16_ext() nes_pokestr_ext() 配列 count() 15 -
ファミコンでのスクロール • ファミコンの処理速度では60fpsで全画面を書き換えられない • 書き換えられるのはネイティブコードでも5行分ぐらい • そもそもBGは8x8の単位でしか配置できない • そのかわり画面全体を1px単位でスクロールできる •
上下左右がつながっていてスクロールアウトした部分は 反対側から出てくる • ファミコンの画面スクロールはこんな風にやってる: • スクロールしたい方向に1pxずつ8回スクロールする • 反対側から出てきたところだけ書き換える ファミコンでPHPを動かす 長谷川智希 @tomzoh 19 -
動作速度とメモリが足りない • PHPで素直にやるとすごく遅くなる • 素直にやる: 1pxずつスクロール→8pxスクロールしたらはみ出した部分を上書き • さらにスプライト(街の人々)をスクロールといっしょに動かす処理がツラい • ファミコンの画面はBG面(背景)の上にスプライト(キャラクタ)が乗っている
• ファミコンのスクロール、「BG面面をスクロールする」なのでスプライトは置き去りになる • 1pxスクロールしたら全スプライトも1px移動する必要がある • 処理をたくさんする必要がある • けど全画面書き換えるよりは全然マシ • メモリも足りない • ↑の動作をPHPで書いてファミコン上でコンパイルしてopcodeにするとメモリに乗り切ら ない • これはnesphpのロマン「ファミコン上でPHPソースをコンパイルしたい」との衝突 ファミコンでPHPを動かす 長谷川智希 @tomzoh 26 -
解決方法 • ネイティブでやる関数を作った • nes_cam_move($dx, $dy, $frames) / nes_cam_wait() •
$dx, $dy 方向に $frames 連続でスクロールさせる / 移動が終わるまで待つ • 実行するとすぐ帰ってくる(非同期で動く) • スプライトもいっしょに動かす • 「これはPHPだろうか」 • 「PHPで難しいことを別の方法でやるのは我々の得意技」 ファミコンでPHPを動かす 長谷川智希 @tomzoh 27 -
nesphp • 今回のコードはGitHubに置いてあります • https://github.com/hasegawa-tomoki/nesphp • macOS上でもファミコンエミュレータで動かせるので興味ある方は遊んでみてください • デモで使用したスクロール系デモは examples/
にあります ファミコンでPHPを動かす 長谷川智希 @tomzoh 29 -
余談: php-terminal-nes-emulator • PHPで書かれたファミコンエミュレータ • 冒頭で紹介したもの • ファミコンのROMイメージを実行できる • …ということは…?
• PHPで書かれたファミコンエミュレータ上で ファミコンで動くPHPが動く? • もちろん動きます✌ ファミコンでPHPを動かす 長谷川智希 @tomzoh 30 -
𝕏 まとめ 長谷川智希 • ファミコンでPHPを動かす nesphp を作った @tomzoh • PHPの基本文法と、ファミコン特有の独自命令を実装した
• 動作速度とメモリ不足に苦しんだ • とは言いつつ、そこそこPHPらしく、ファミコンぽいものを実現できるぐらいのものができた • はじめてClaude Codeで趣味DIYプロジェクトやった • AIは深い知識をもっており、試行錯誤が速いのですごい勢いで実装が進む • AIが悩んでいるところに意見して相談して良いモノが出来るとすごく楽しい • のだけど、これ、一番書いてるの日本語だし、仕事みたいだな… • 役に立つものを作る時には最高だけど役に立たないものを作るのにはどうだろうな… • でもまあファミコンでPHP動くのは楽しい • みんなもヘンなとこで動くPHP作ってみて ファミコンでPHPを動かす 長谷川智希 @tomzoh 31 -
𝕏 The other version... • PHPカンファレンス2026でも同じ話で採択頂いていました • テトリスを作って直面したメモリ不足とその解決方法についてお話しています • 今日の話が面白かった、もっと聞きたいという方は
Speaker Deckで公開済の資料やYouTubeアーカイブご覧ください https://www.youtube.com/live/skFNr05xenc? si=iunI1k6eNsm6H26P&t=11687 長谷川智希 ファミコンでPHPを動かす 長谷川智希 @tomzoh @tomzoh 32 -
off. sz. field 0 8 handler opcode構造体 opcodeハンドラの C関数ポインタ 8
4 op1 constant / var / num / jmp_offset • opcodeはメモリ上には32bytesのデータとして格 納されている 12 4 op2 • つまりPHPの1命令は最低でも32bytesのメモリを必要 16 4 result 20 4 extended_value とする • for()みたいな制御構造は複数のopcodeで実現される • nesphpではROMにPHPソースコードがある 第3引数・フラグ等の 追加情報 24 PHPソース行番号 • ソースコードをコンパイルした結果のopcodeはRAM上に 置く必要がある ファミコンでPHPを動かす (デバッグ用) 28 • ファミコンのRAMは2KB = 2,048bytes • つまり64命令しか置けない 長谷川智希 @tomzoh 4 lineno 1 opcode 命令番号 29 1 op1_type IS_CONST / IS_CV など 30 1 op2_type 31 1 result_type 36 -
opcodeを小さくする off. sz. field 0 8 handler sz. opcodeハンドラの C関数ポインタ
8 • nesphpで不要なものを削り、フィールドサイズも削減する • handler • このopcodeを処理するC関数へのポインタが入っている • nesphpではswitch-case的に処理するので不要 4 op1 2 constant / var / num / jmp_offset 12 4 op2 2 16 4 result 2 20 4 extended_value 2 第3引数・フラグ等の 追加情報 • lineno • デバッグ用にPHPソースの行番号が入っている • コンパイル時はコンパイラが出せるのでいらない • 実行時エラーは行番号出せなくなる 24 4 lineno PHPソース行番号 (デバッグ用) 28 1 opcode 1 命令番号 • op1, op2, result, extended_value 29 • nesphpでは指し示す先が1byteで済む 30 1 op2_type 1 31 1 result_type 1 • 32bytes → 12bytes ファミコンでPHPを動かす 長谷川智希 @tomzoh 1 op1_type 1 IS_CONST / IS_CV など 38 -
PHPの変数表現 変数 zval off. sz. field 0 8 value •
PHPの値は内部的にzvalで表現される • 変数1こ 最低16bytes 値 or 値へのポインタ 8 型情報 12 4 u2 用途に応じた補助情報 • 配列の場合、もうちょっと複雑 • zval → zend_array → Bucket × n 構造 • zend_array にはインデックスの次の値とかが入っている • テトリスを考えてみる • 縦20マス × 横10マス: 200要素 • zval 16bytes + zend_array 64bytes + Bucket 32bytes × 200 = 6,480bytes • 我々が持っているメモリは2KB, 2,480bytes ファミコンでPHPを動かす 4 u1 長谷川智希 @tomzoh 配列 zval 16 bytes zend̲array 64 bytes Bucket 32 bytes Bucket 32 bytes Bucket 32 bytes 40 -
nesphpでの工夫 - 変数 変数 zval off. sz. field 0 8
value sz. 3 値 or 値へのポインタ • zvalを8bytes → 4bytesに 8 • valueは2bytes + 1bytes: intの最大値が16bitになる • 玄人用: • nesphpではzend_stringを廃止している • RAM上に文字列そのものがあるのでポインタはRAMを指す • 文字長だけvalueのbyte 3に持たせている • 配列も大きく削減 • zend_arrayからGCなどパフォーマンスのための構造を全削除 4 u1 1 型情報 12 4 u2 - 用途に応じた補助情報 配列 zval 16 bytes zend̲array 64 bytes Bucket 32 bytes Bucket 32 bytes Bucket 32 bytes 64bytes → 4bytes • 連想配列をサポートしないのでBucketはzvalだけに削減 32bytes → 4bytes • テトリス配列 4 + 4 + 4 × 200 = 808bytes • 6,480bytes → 808bytes ファミコンでPHPを動かす 長谷川智希 @tomzoh 42 -
余談: ファミコンの拡張性 $FFFF • ファミコン、カートリッジとCPUがアドレス線16本つながって $C000 る • ファミコン発売時のソフトたちはROM 16KBだった
プログラムROM - 16KB プログラムROM - 16KB • アドレス線14本で表現できる • 16本つなげておいたことで拡張性が得られた • SOUND IN/OUTなんかもカートリッジとつながってる • カートリッジ側に音源チップを載せてリッチな音を鳴らすなんての も $8000 $6000 I/Oポート • ファミコンはカートリッジ側にも機能がいろいろある (APU, etc) • ファミコンエミュレータはファミコン本体の機能だけエミュレートして I/Oポート いては市販されたゲームは動かせない (PPU) • ファミコンエミュレート + カートリッジ機能エミュレート + カートリ ッジ内蔵データ ファミコンでPHPを動かす バッテリー バックアップRAM - 8KB 長谷川智希 @tomzoh $07FF $0000 ワークRAM - 2KB 43 -
ファミコン • 1983年 任天堂から発売されたゲーム機 • 当時 14,800円 • 世界で1.15億台販売 •
CPUはリコー RP2A03 • 6502(ほぼ)互換の派生CPU • 6502 - BCD演算 + APU(サウンド) + DMA + I/O • 6502: Apple I, Apple II • PPU(今で言うGPU)もリコーで RP2C02 ファミコンでPHPを動かす 長谷川智希 @tomzoh 44 -
ファミコンで自作プログラムを動かす • EVERDRIVE • ファミコンで自作プログラムを動かせるアイテム • microSDカードにROMイメージを入れてファミコンを起動 • Amazonで売ってる •
…けどどうもこれは本家EVERDRIVEの互換品みたい • だいたい動くけどしばしば不安定… • オリジナルを直販サイトからも買ってみた • ウクライナから2週間ぐらいかけて届いた • 安定してるので今日のデモはこっちでやってます ファミコンでPHPを動かす 長谷川智希 @tomzoh 46 -
時代のギャップ埋め • ファミコンは1983年発売のデバイス • 当時のテレビはアナログ • ファミコン、RF出力しか無い • アナログテレビの1ch/2chに信号を割り込ませるやつ •
軽めの改造でAV端子(黄白赤)にできる • 電源のUSB-C化といっしょにやって背面パネルを3D プリンタで作った https://news.denfaminicogamer.jp/kikakuthetower/181226a/2 ファミコンでPHPを動かす 長谷川智希 @tomzoh 47 -
ファミコンのCPUとメモリ • ファミコンのCPUはリコー RP2A03 • アドレス線16本、データ線8本 • アドレス 16bit, データ
8bit のメモリアクセスができる • アドレス 0x0000 〜 0xffff のどれかを指定して… • 読み込めば1byteのデータが返ってくる • 書き込めば1byteのデータを書き込める • 合計64KB https://ja.wikipedia.org/wiki/MOS_6502 https://user.xmission.com/~trevin/atari/6502_pinout.html ファミコンでPHPを動かす 長谷川智希 @tomzoh 49 -
いまどきのコンピュータとの違い $FFFF • いまどきのコンピュータはROMはほぼ使われない $C000 • 電源投入直後に動くようなプログラム(BIOS)ぐらい • プログラムはSSDなどに保存してあって、 プログラムROM
- 16KB プログラムROM - 16KB それをRAMに読み込んで実行する • ファミコンではプログラムはROMに保存されて CPUに直結されている • 書換可能な領域と書換不可な領域に何を保存するかを $8000 $6000 考える必要がある バッテリー バックアップRAM - 8KB I/Oポート (APU, etc) • SwitchとかはソフトウェアがROMで供給されているけど ストレージ扱いでありCPUには直結されていない I/Oポート (PPU) $07FF $0000 ファミコンでPHPを動かす 長谷川智希 @tomzoh ワークRAM - 2KB 50 -
ファミコンのメモリマップ • どのアドレスに何が接続されているかのマップを メモリマップと呼ぶ • $6000-$FFFF ゲームカートリッジ • プログラムROM 32KB
読み取り専用メモリ ゲームカートリッジ • ファミコンのCPUにはいろいろなデバイスが接続されている $FFFF $C000 $8000 • バッテリーバックアップRAM 8KB 電源を切っても内容を保持するメモリ セーブデータ的な用途がメイン $6000 • APU, PPU: 映像とかサウンドのチップ • ワークRAM 2KB プログラムROM - 16KB バッテリー バックアップRAM - 8KB I/Oポート (APU, etc) • いまどきのPCで言う「メモリ」 • キャラクタの座標とか加速度とかゲーム進行に必要なデータを保存する • 変数をイメージするとわかりやすいかも ファミコンでPHPを動かす プログラムROM - 16KB 長谷川智希 @tomzoh I/Oポート (PPU) $07FF $0000 ワークRAM - 2KB 52 -
RAM不足 • RAMが全然足りない • ざっと作ったテトリスをコンパイルしたopcodeは18,592bytes • opcode 1個あたり32bytes × 581個
• テトリスの盤面 10 × 20マス配列のzval表現は 6,480bytes • これだけで25,072bytes必要 • ファミコンのワークRAMは2,048bytes… ファミコンでPHPを動かす 長谷川智希 @tomzoh 53 -
opcode/zval 縮小 • 「PHPらしさ」として内部的にopcode / zvalを使うことにしたけどファミコン の手に余るサイズ • nesphpでは使い道がないフィールドや過剰なサイズを持っているフィールドが あるので削除/縮小した
• opcode • zval • 配列要素 ファミコンでPHPを動かす 32bytes → 12bytes 8bytes → 4bytes 32bytes → 4bytes 長谷川智希 @tomzoh 54 -
工夫後のテトリス • プログラム • opcode • リテラル 581個 × 12bytes
= 6,972bytes 45個 × 4bytes = 180bytes 53個 × 4bytes = 212bytes 4bytes + 22 × 4bytes = 92bytes • 変数 • スカラ変数 • 配列1個22要素 • 1マス1配列要素でなく、1マス=1bitに圧縮した • 合計7,166bytes • ファミコンのワークRAMは2,048bytes • これでもぜんぜん足りない ファミコンでPHPを動かす 長谷川智希 @tomzoh 55 -
もう1つのRAM領域 • ファミコンはセーブデータを保存する機能が(本体としては)無いが、 カートリッジ側に回路を積んで機能拡張できる • カートリッジ上にRAMと電池を置いてセーブデータを保存する様なのがある • ドラゴンクエストIIIとか • 各メーカー好きなように拡張してた
• カートリッジを作れば好きなように拡張できる • 今回はEVERDRIVEがサポートするマッパーで実装する • メモリに関する機能拡張をマッパーと呼ぶ • ROM容量も厳しいのでいっしょに拡張できるとGood ファミコンでPHPを動かす 長谷川智希 @tomzoh 57 -
マッパー1(MMC1) SXROM $FFFF • バッテリーバックアップRAMを持つマッパー $C000 • $6000-$8000 の8KBがRAM領域 •
バンク切替という方法で合計32KB使える • 特定のアドレスにデータを書き込むと $6000-$8000 の内容 が別のバンクに切り替わる • いまどきのPCもこれに似た仕組み: CPUからは4GBしか見えない • バンク0〜バンク3の4バンクある $8000 $6000 プログラムROM - 16KB プログラムROM - 16KB バッテリー バックアップRAM - 8KB I/Oポート RAM バンク 用途 0 コンパイル結果のopcodeとリテラル 1 配列 2 文字列リテラルの本体 3 拡張RAM (nes_poke_ext(), nes_peek_ext() で読み書きできる) ファミコンでPHPを動かす 長谷川智希 @tomzoh (APU, etc) I/Oポート (PPU) $07FF $0000 ワークRAM - 2KB 58 -
マッパー1(MMC1) SXROM • このマッパー、ROMも拡張できる $FFFF PHPコンパイラ + PHP VM +
もろもろ $C000 • バンク4つ、合計64KB使える PHPソース or キャラクタデータ • nesphpでは3バンク使っている • もっとデカイPHPソースも置ける • がコンパイル結果をRAMに置ききれるか…問題も $8000 $6000 バンク 内容 0(切替) PHP ソース 1(切替) キャラクタデータ 2 未使用 3($C000 固定) PHPコンパイラ + PHP VM + もろもろ(割り込みベクタ) ファミコンでPHPを動かす プログラムROM - 16KB プログラムROM - 16KB バッテリー バックアップRAM - 8KB I/Oポート (APU, etc) I/Oポート (PPU) 長谷川智希 @tomzoh $07FF $0000 ワークRAM - 2KB 59 -
テトリスのメモリ使用状況 実行できるプログラムサイズの 上限が近い • ROM • バンク0: テトリスのPHPソースコード 14,554bytes /
16,384bytes (88.8%) • バンク3: PHPコンパイラ, PHP VM 13,815bytes / 16,384bytes (84.3%) • RAM • 本体: スタック, PHP変数, … • バンク0: opcode, リテラル • バンク1: 配列 • バンク2: 文字列リテラル実体 • バンク3: 拡張RAM ファミコンでPHPを動かす 実装できる機能の 上限が近い (83.0%) 7,168bytes / 8,192bytes (87.5%) 92bytes / 8,192bytes (1.1%) 100bytes / 8,192bytes (1.2%) 220bytes / 8,192bytes (2.7%) 長谷川智希 @tomzoh 62