Slide 1

Slide 1 text

PCOVから学ぶコードカバレッジ PHPカンファレンス小田原2026 Hideki Kinjyo GitHub: o0h / X: @o0h_ [公開用] v1.3.6

Slide 2

Slide 2 text

こんにちは〜〜〜〜〜〜!! 2

Slide 3

Slide 3 text

このトークのプロポーザル 3

Slide 4

Slide 4 text

カバレッジってなんだ? 4

Slide 5

Slide 5 text

カバレッジってなんだ? 5

Slide 6

Slide 6 text

明日から使える豆知識が増えましたね! (おめでとうございます) 6

Slide 7

Slide 7 text

(話が逸れたぜ!) 閑話休題 7

Slide 8

Slide 8 text

ここまでのまとめ 1. タイトル 2. 挨拶 3. トークの紹介(forteeから) 4. 豆知識 8

Slide 9

Slide 9 text

ここまでのまとめ 1. タイトル 2. 挨拶 3. トークの紹介(forteeから) 4. 豆知識 9 実行されたプログラム

Slide 10

Slide 10 text

ここまでのまとめ 10 実行されたプログラム 1. タイトル 2. 挨拶 3. トークの紹介(forteeから) 4. 豆知識 プログラムと スライドを 対応付けてみると?

Slide 11

Slide 11 text

1. タイトル 2. 挨拶 3. トークの紹介(forteeから) 4. 豆知識 実行されたプログラム ロジックと「実装」ソース 11 #1 #2 #3 #4 #5 #6

Slide 12

Slide 12 text

ロジックと「実装」ソース 12 カバーされたスライド 1. タイトル 2. 挨拶 3. トークの紹介(forteeから) 4. 豆知識 実行されたプログラム #1 #2 #3 #4 #5 #6

Slide 13

Slide 13 text

ロジックと「実装」ソース 13 「未実行」なスライドもある #1 #2 #3 #4 #5 #6 #?? #?? #?? #?? #??

Slide 14

Slide 14 text

普段のプログラム実行においては (実行の結果として)何が出力されたかが大事 14

Slide 15

Slide 15 text

普段のプログラム実行においては (実行の結果として)何が出力されたかが大事 どこが実行された/されなかったかって 意識もされない(できない)ですよね・・・? 15

Slide 16

Slide 16 text

このトークのプロポーザル 16

Slide 17

Slide 17 text

このトークのプロポーザル 17

Slide 18

Slide 18 text

このトークのプロポーザル 18

Slide 19

Slide 19 text

このトークのプロポーザル 考えてみたら 不思議な感じがする 19

Slide 20

Slide 20 text

通常の実行では使われない?捨てられる?データを 追加で収集していそうだ 20

Slide 21

Slide 21 text

通常の実行では使われない?捨てられる?データを 追加で収集していそうだ なんかPHP拡張ってのを使っているらしいな! 21

Slide 22

Slide 22 text

今日のお話 カバレッジ測定を通じて、 「PHP拡張は、どんな機能を提供しているのか」 「何を可能にしているのか」 を知っていきましょう (※ なるべくCのコードには触れません) 22

Slide 23

Slide 23 text

自己紹介 • 金城秀樹 / きんじょうひでき • GitHub: @o0h / 𝕏 : @o0h_ • 好きなFWはCakePHP • アイコンは美味しい鮭親子丼の写真です • 最近はPodcastをやっています • ハッシュタグ: #readlinefm 

Slide 24

Slide 24 text

おしながき 1. pcovの基本的な使い方や機能を見てみる 2. 実行されたファイル・行を記録する仕組み 3. 実行済み・未実行のファイル・行を分別する仕組み 4. まとめ 24

Slide 25

Slide 25 text

① pcovの基本的な使い方や機能を見てみる ② 実行されたファイル・行を記録する仕組み ③ 実行済み・未実行のファイル・行を分別する仕組み ④ まとめ

Slide 26

Slide 26 text

カバレッジを測定しよう!! #とは

Slide 27

Slide 27 text

27 こういうやつの話

Slide 28

Slide 28 text

実行されていない 実行されていない 何もしていない

Slide 29

Slide 29 text

実行された 実行された 関数を呼んだ

Slide 30

Slide 30 text

実行された if条件網羅

Slide 31

Slide 31 text

pcovによるカバレッジの測定 (起動・終了・収集)

Slide 32

Slide 32 text

測定実行コード pcovの関数を利用して カバレッジを測定する コード 32

Slide 33

Slide 33 text

測定実行コード 33

Slide 34

Slide 34 text

測定実行コード 34

Slide 35

Slide 35 text

測定実行コード 35

Slide 36

Slide 36 text

測定実行コード 36

Slide 37

Slide 37 text

測定結果 37 ["/opt/lib/functions.php"]=> array(4) { [13]=> int(-1) [7]=> int(1) [8]=> int(-1) [11]=> int(1) } $coverage = \pcov\collect(); var_dump($coverage); \pcov\collect()の 実行結果をdumpしたもの

Slide 38

Slide 38 text

測定結果 38 ["/opt/lib/functions.php"]=> array(4) { [13]=> int(-1) [7]=> int(1) [8]=> int(-1) [11]=> int(1) } 対象ファイル 対象行 測定結果

Slide 39

Slide 39 text

測定結果 39 ["/opt/lib/functions.php"]=> array(4) { [13]=> int(-1) [7]=> int(1) [8]=> int(-1) [11]=> int(1) } 1回でもその行を通った 1回もその行を通らなかった

Slide 40

Slide 40 text

測定結果 40 [13]=> int(-1) [7]=> int(1) [8]=> int(-1) [11]=> int(1)

Slide 41

Slide 41 text

pcovが提供している(その他の)PHP関数

Slide 42

Slide 42 text

pcov拡張が提供している関数 \pcov\start, \pcov\stop, \pcov\collect Ҏ֎ʹ΋ɾɾ • \pcov\enabled : pcov͕༗ޮʹͳ͍ͬͯΔ͔Λฦ͢ • \pcov\clear : ֨ೲ͞Ε͍ͯΔ৘ใΛΫϦΞ • \pcov\waiting : collect͞Ε͍ͯͳ͍ϑΝΠϧϦετΛฦ͢ • \pcov\memory : ֬อࡁΈͰ࢖ΘΕ͍ͯͳ͍ϝϞϦ༰ྔΛฦ͢ 42

Slide 43

Slide 43 text

① pcovの基本的な使い方や機能を見てみる ② 実行されたファイル・行を記録する仕組み ③ 実行済み・未実行のファイル・行を分別する仕組み ④ まとめ

Slide 44

Slide 44 text

pcovが「できること」は分かったので それを「どうやっているのか」、 「何を使っているのか」の話 44

Slide 45

Slide 45 text

先にネタバレ 実行されたファイル・行を記録する仕組み • PHPでは、元のコードを低次元な形に変換した上で(opcode)、 • それを1ステップずつ実行される際に、 • pcov独自の処理を差し込んで • 「元のソースコードのどこの行か」を記録しておく 45

Slide 46

Slide 46 text

先にネタバレ 実行されたファイル・行を記録する仕組み • PHPでは、元のコードを低次元な形に変換した上で(opcode)、 • それを1ステップずつ実行される際に、 • pcov独自の処理を差し込んで • 「元のソースコードのどこの行か」を記録しておく 46

Slide 47

Slide 47 text

順番に見ていきましょう 1. 低次元な形に変換する 2. 1ステップずつ実行する(際に) 3. 処理を差し込んで 4. 元のソースコードのどこの行かを記録 47

Slide 48

Slide 48 text

順番に見ていきましょう 1. 低次元な形に変換する 2. 1ステップずつ実行する(際に) 3. 処理を差し込んで 4. 元のソースコードのどこの行かを記録 48

Slide 49

Slide 49 text

PHPスクリプトが実行されるまで 49 PHPのコード (いつもの) 本当のCPU (何か良い感じにする)

Slide 50

Slide 50 text

PHPスクリプトが実行されるまで 50 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM 機械語 (何か良い感じにする)

Slide 51

Slide 51 text

PHPスクリプトが実行されるまで 51 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM OpCode 機械語 Zend Compiler

Slide 52

Slide 52 text

PHPスクリプトが実行されるまで 52 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM OpCode 機械語 Zend Compiler 低次元に変換されたコード

Slide 53

Slide 53 text

りある・おぺこーど

Slide 54

Slide 54 text

PHPスクリプトが実行されるまで 54 本当のCPU 仮想マシン ZendVM 機械語 $_main: L10 00 INIT_FCALL 2 144 string("f") L10 01 SEND_VAL int(2) 1 L10 02 SEND_VAL int(3) 2 L10 03 V2 = DO_FCALL L10 04 ASSIGN CV0($a) V2 L11 05 INIT_FCALL 2 144 string("f") L11 06 SEND_VAL int(2) 1 L11 07 SEND_VAL int(0) 2 L11 08 V4 = DO_FCALL L11 09 ASSIGN CV1($b) V4 L12 10 RETURN int(1) f: L02 00 CV0($a) = RECV 1 L02 01 CV1($b) = RECV 2 L04 02 T2 = IS_EQUAL CV1($b) int(0) L04 03 JMPZ T2 05 L05 04 RETURN null L08 05 T3 = DIV CV0($a) CV1($b) L08 06 RETURN T3 L09 07 RETURN null

Slide 55

Slide 55 text

out-of-scope • これがなぜ「何か良い感じに」なのかは、 PHPerKaigi 2026の発表を参照してください • 何となく伝わる気がします 55 https://speakerdeck.com/o0h/phperkaigi-2026

Slide 56

Slide 56 text

順番に見ていきましょう 1. 低次元な形に変換する 2. 1ステップずつ実行する(際に) 3. 処理を差し込んで 4. 元のソースコードのどこの行かを記録 56

Slide 57

Slide 57 text

PHPスクリプトが実行されるまで 57 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM OpCode 機械語 Zend Compiler さっきの図

Slide 58

Slide 58 text

PHPスクリプトが実行されるまで 58 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM OpCode 機械語 Zend Compiler さっきの図 ココで起きていること

Slide 59

Slide 59 text

ZendVMのメインループ • 起点となるフレームを受け取る • その中にオペコードのリストがある • リストの順に実行していく 59 仮想マシン ZendVM OpCode

Slide 60

Slide 60 text

ZendVMのメインループ 60 $_main: L10 00 INIT_FCALL 2 144 string("f") L10 01 SEND_VAL int(2) 1 L10 02 SEND_VAL int(3) 2 L10 03 V2 = DO_FCALL L10 04 ASSIGN CV0($a) V2 L11 05 INIT_FCALL 2 144 string("f") ɾ ɾ ɾ 仮想マシン ZendVM 丸っと受け取る op_array

Slide 61

Slide 61 text

ZendVMのメインループ 61 L10 00 INIT_FCALL 2 144 string("f") 仮想マシン ZendVM 1ステップずつ実行 L10 01 SEND_VAL int(2) 1 L10 02 SEND_VAL int(3) 2 L10 03 V2 = DO_FCALL ɾ ɾ ɾ opline

Slide 62

Slide 62 text

ZendVMのメインループ 62 L10 00 INIT_FCALL 2 144 string("div") ɾ ɾ ɾ L10 03 V2 = DO_FCALL 仮想マシン ZendVM div: L02 00 CV0($a) = RECV 1 L02 01 CV1($b) = RECV 2 L04 02 T2 = IS_EQUAL CV1($b) int(0) ɾ ɾ ɾ 別のフレームの実行

Slide 63

Slide 63 text

zend_execute_ex 「いち関数実行(メイン関数を含む)」ごとに、 `zend_execute_ex` が実行される 63 $_main: 仮想マシン ZendVM div: zend_execute_ex zend_execute_ex ※ このあたり、拡張の利用有無で挙動が変わることがある

Slide 64

Slide 64 text

zend_execute_ex (っぽいもの) zend_execute_exは、1つのフレームに相当する単位で 実行するデータを受け取り、1ステップずつ処理していく 64 function zend_execute_ex($࣮ߦ͢Δσʔλ) { $op_array = $࣮ߦ͢Δσʔλ->ίϨΫγϣϯ; foreach ($op_array as $opline) { εςοϓ͝ͱͷॲཧ($opline); } } PHPer向けの ざっくりイメージ ※ このあたり、拡張の利用有無で挙動が変わることがある

Slide 65

Slide 65 text

↑ここまでが「通常のPHP」の処理の話 ↓やっと「PHP拡張」の話が出てきます!!! 65

Slide 66

Slide 66 text

順番に見ていきましょう 1. 低次元な形に変換する 2. 1ステップずつ実行する(際に) 3. 処理を差し込んで 4. 元のソースコードのどこの行かを記録 66

Slide 67

Slide 67 text

PHP拡張とフック • zend_execute_exの処理を任意のものに差し替えられる • 拡張側で関数ポインタをオーバーライドする 67 標準的な機能をラップする形で、 「プラスα」なものに差し替えるなど

Slide 68

Slide 68 text

PHP拡張とフック • zend_execute_exの処理を任意のものに差し替えられる • 拡張側で関数ポインタをオーバーライドする • PHP実行ライフサイクル中にフックがある • これらのフックに引っ掛けて、ゴニョゴニョする 68 フック = PHPUnitでおなじみの setUpとかtearDownみたいなやつね!!

Slide 69

Slide 69 text

PHP拡張とフック • zend_execute_exの処理を任意のものに差し替えられる • 拡張側で関数ポインタをオーバーライドする • PHP実行ライフサイクル中にフックがある • これらのフックに引っ掛けて、ゴニョゴニョする • 提供されているのは、 モジュールの初期化時・リクエスト処理の初期化時…など 69

Slide 70

Slide 70 text

pcovのオーバーライド PHP_MINIT_FUNCTION(pcov) { /** লུ */ if (php_pcov_api_enabled()) { zend_execute_ex_function = zend_execute_ex; zend_execute_ex = php_pcov_execute_ex; } ZVAL_LONG(&php_pcov_uncovered, PHP_PCOV_UNCOVERED); ZVAL_LONG(&php_pcov_covered, PHP_PCOV_COVERED); /** লུ */ return SUCCESS; }  pcov.c

Slide 71

Slide 71 text

pcovのオーバーライド PHP_MINIT_FUNCTION(pcov) { /** লུ */ /** লུ */ }  PHP_MINIT_FUNCTION(pcov) 「モジュールの初期化」時に 処理をぶら下げるフック のおまじない(マクロ) pcov.c

Slide 72

Slide 72 text

pcovのオーバーライド PHP_MINIT_FUNCTION(pcov) { /** লུ */ /** লུ */ }  if (php_pcov_api_enabled()) { モジュールを読み込んでいても、iniファ イル等でdisableされているからね pcov拡張が有効かをチェック pcov.c

Slide 73

Slide 73 text

pcovのオーバーライド PHP_MINIT_FUNCTION(pcov) { /** লུ */ /** লུ */ }  zend_execute_ex_function = zend_execute_ex; 終了時のフックで 復帰に使う 元の処理を退避させて pcov.c

Slide 74

Slide 74 text

pcovのオーバーライド PHP_MINIT_FUNCTION(pcov) { /** লུ */ /** লུ */ }  zend_execute_ex = php_pcov_execute_ex; pcovの独自処理に 差し替える pcov.c

Slide 75

Slide 75 text

余談 PHP_MINIT_FUNCTION(pcov) { /** লུ */ /** লུ */ }  ZVAL_LONG(&php_pcov_uncovered, PHP_PCOV_UNCOVERED); 「実行されていない行」 を示す定数、「-1」 ZVAL_LONG(&php_pcov_covered, PHP_PCOV_COVERED); 「実行されている行」 を示す定数、「1」 pcov.c

Slide 76

Slide 76 text

順番に見ていきましょう 1. 低次元な形に変換する 2. 1ステップずつ実行する(際に) 3. 処理を差し込んで 4. 元のソースコードのどこの行かを記録 76

Slide 77

Slide 77 text

zend_execute_ex / _zend_execute_data zend_execute_exは_zend_execute_dataを受け取る 77 ZEND_API extern void (*zend_execute_ex) (zend_execute_data *execute_data); struct _zend_execute_data { const zend_op *opline; ɾɾɾ zval *return_value; zend_function *func; }; zend_execute.h zend_execute.h

Slide 78

Slide 78 text

zend_execute_ex / _zend_execute_data zend_execute_dataはzend_functionをもち、op_arrayを含む 1つのop_arrayが1つのフレームに相当(≒関数の中身のopcode) 78 struct _zend_execute_data { const zend_op *opline; ɾɾɾ zend_function *func; }; union _zend_function { uint8_t type; uint32_t quick_arg_flags; struct {...} common; zend_op_array op_array; ɾɾɾ }; zend_types.h zend_types.h

Slide 79

Slide 79 text

• 先ほど見たように、pcovを使うと zend_execute_exとしてphp_pcov_execute_exが呼び出される • この中身を追っていくことで、 どうやって実行された行を記録しているのか分かる(遂に!) php_pcov_execute_ex 79 zend_execute_ex = php_pcov_execute_ex;

Slide 80

Slide 80 text

php_pcov_execute_ex void php_pcov_execute_ex(zend_execute_data *execute_data) { int zrc = 0; while (1) { zrc = php_pcov_trace(execute_data); if (zrc != SUCCESS) { if (zrc < SUCCESS) { return; } execute_data = EG(current_execute_data); } } }  pcov.c

Slide 81

Slide 81 text

php_pcov_execute_ex (っぽいもの) function pcovExecuteEx($࣮ߦ͢Δσʔλ) { while (true) { $returnCode = pcovTrace($࣮ߦ͢Δσʔλ); if ($returnCode === CODE_શ෦ऴྃ) { return; } elseif ($returnCode !== CODE_ͦͷ··࣍΁) { $࣮ߦ͢Δσʔλ = ݱࡏͷϑϨʔϜऔಘ(); } } } 

Slide 82

Slide 82 text

php_pcov_execute_ex (っぽいもの) function pcovExecuteEx($࣮ߦ͢Δσʔλ) { while (true) { $returnCode = pcovTrace($࣮ߦ͢Δσʔλ); if ($returnCode === CODE_શ෦ऴྃ) { return; } elseif ($returnCode !== CODE_ͦͷ··࣍΁) { $࣮ߦ͢Δσʔλ = ݱࡏͷϑϨʔϜऔಘ(); } } }  while (true) { 全部終わるまで反復

Slide 83

Slide 83 text

php_pcov_execute_ex (っぽいもの) function pcovExecuteEx($࣮ߦ͢Δσʔλ) { while (true) { $returnCode = pcovTrace($࣮ߦ͢Δσʔλ); if ($returnCode === CODE_શ෦ऴྃ) { return; } elseif ($returnCode !== CODE_ͦͷ··࣍΁) { $࣮ߦ͢Δσʔλ = ݱࡏͷϑϨʔϜऔಘ(); } } }  pcovTrace($࣮ߦ͢Δσʔλ); opcodeを1ステップずつ実行 対応するPHPスクリプトの行を記録 実行結果状態を返す

Slide 84

Slide 84 text

php_pcov_execute_ex (っぽいもの) function pcovExecuteEx($࣮ߦ͢Δσʔλ) { while (true) { $returnCode = pcovTrace($࣮ߦ͢Δσʔλ); if ($returnCode === CODE_શ෦ऴྃ) { return; } elseif ($returnCode !== CODE_ͦͷ··࣍΁) { $࣮ߦ͢Δσʔλ = ݱࡏͷϑϨʔϜऔಘ(); } } }  if ($returnCode === CODE_શ෦ऴྃ) { return; } elseif ($returnCode !== CODE_ͦͷ··࣍΁) { $࣮ߦ͢Δσʔλ = ݱࡏͷϑϨʔϜऔಘ(); } 状態に応じて 離脱したり次に行ったり

Slide 85

Slide 85 text

php_pcov_trace static zend_always_inline int php_pcov_trace(zend_execute_data *execute_data) { if (PCG(enabled)) { if (php_pcov_wants(EX(func)->op_array.filename) && !php_pcov_ignored_opcode(EX(opline)->opcode) && !php_pcov_has(EX(func)->op_array.filename, EX(opline)->lineno)) { php_coverage_t *coverage = php_pcov_create(execute_data); if (!PCG(start)) { PCG(start) = coverage; } else { *(PCG(next)) = coverage; } PCG(next) = &coverage->next; } } return zend_vm_call_opcode_handler(execute_data); }  pcov.c

Slide 86

Slide 86 text

php_pcov_trace (っぽいもの) function pcovTrace($࣮ߦ͢Δσʔλ) { if (pcovଌఆελʔτࡁΈ()) { if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) { $ଌఆσʔλ = pcovCreate($࣮ߦ͢Δσʔλ); ଌఆσʔλ௥Ճ($ଌఆσʔλ); } } return zendVMͷඪ४Φϖίʔυॲཧ($࣮ߦ͢Δσʔλ); } 

Slide 87

Slide 87 text

php_pcov_trace (っぽいもの) function pcovTrace($࣮ߦ͢Δσʔλ) { if (pcovଌఆελʔτࡁΈ()) { if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) { $ଌఆσʔλ = pcovCreate($࣮ߦ͢Δσʔλ); ଌఆσʔλ௥Ճ($ଌఆσʔλ); } } return zendVMͷඪ४Φϖίʔυॲཧ($࣮ߦ͢Δσʔλ); }  if (pcovଌఆελʔτࡁΈ()) { pcov\start()するとフラグが立つ / pcov\stop()するとフラグが折れる 「スタート済み」フラグが 立っていれば処理に入る

Slide 88

Slide 88 text

php_pcov_trace (っぽいもの) function pcovTrace($࣮ߦ͢Δσʔλ) { if (pcovଌఆελʔτࡁΈ()) { if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) { $ଌఆσʔλ = pcovCreate($࣮ߦ͢Δσʔλ); ଌఆσʔλ௥Ճ($ଌఆσʔλ); } } return zendVMͷඪ४Φϖίʔυॲཧ($࣮ߦ͢Δσʔλ); }  if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) {

Slide 89

Slide 89 text

php_pcov_trace (っぽいもの) - 測定データの追加 if (ଌఆର৅ϑΝΠϧ͔($࣮ߦ͢Δσʔλ->func->ϑΝΠϧ໊) && !ແࢹ͢ΔΦϖίʔυ͔($࣮ߦ͢Δσʔλ->ݱࡏͷopline) && !طʹଌఆࡁΈ͔( $࣮ߦ͢Δσʔλ->func->ϑΝΠϧ໊, $࣮ߦ͢Δσʔλ->ߦ൪߸ )) {  if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) {

Slide 90

Slide 90 text

php_pcov_trace (っぽいもの) function pcovTrace($࣮ߦ͢Δσʔλ) { if (pcovଌఆελʔτࡁΈ()) { if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) { $ଌఆσʔλ = pcovCreate($࣮ߦ͢Δσʔλ); ଌఆσʔλ௥Ճ($ଌఆσʔλ); } } return zendVMͷඪ४Φϖίʔυॲཧ($࣮ߦ͢Δσʔλ); }  pcovCreate($࣮ߦ͢Δσʔλ); 「実行済み」行のデータ作成

Slide 91

Slide 91 text

php_pcov_create static zend_always_inline php_coverage_t* php_pcov_create(zend_execute_data *execute_data) { /* {{{ */ php_coverage_t *create = (php_coverage_t*) zend_arena_alloc(&PCG(mem), sizeof(php_coverage_t)); create->file = php_pcov_interned_string(EX(func)- >op_array.filename); create->line = EX(opline)->lineno; create->next = NULL; zend_hash_add_empty_element(&PCG(waiting), create->file); return create; } /* }}} */  pcov.c

Slide 92

Slide 92 text

php_pcov_create - 要点 // php_pcov_create(zend_execute_data *execute_data) { php_coverage_t *create = /** ...* / create->file = php_pcov_interned_string( EX(func)->op_array.filename ); create->line = EX(opline)->lineno; create->next = NULL; return create; } 

Slide 93

Slide 93 text

php_pcov_create - 要点 // php_pcov_create(zend_execute_data *execute_data) { php_coverage_t *create = /** ...* / create->file = php_pcov_interned_string( EX(func)->op_array.filename ); create->line = EX(opline)->lineno; create->next = NULL; return create; }  ファイル名、行番号を持つ 構造体 create->file create->line

Slide 94

Slide 94 text

php_pcov_trace (っぽいもの) function pcovTrace($࣮ߦ͢Δσʔλ) { if (pcovଌఆελʔτࡁΈ()) { if (/** ଌఆσʔλΛ࡞ͬͯ௥Ճ͢Δ͔ */) { $ଌఆσʔλ = pcovCreate($࣮ߦ͢Δσʔλ); ଌఆσʔλ௥Ճ($ଌఆσʔλ); } } return zendVMͷඪ४Φϖίʔυॲཧ($࣮ߦ͢Δσʔλ); }  ଌఆσʔλ௥Ճ($ଌఆσʔλ); file, line情報を持つ 構造体を蓄積する

Slide 95

Slide 95 text

① pcovの基本的な使い方や機能を見てみる ② 実行されたファイル・行を記録する仕組み ③ 実行済み・未実行のファイル・行を分別する仕組み ④ まとめ

Slide 96

Slide 96 text

まずはネタバレ 実行済み・未実行のファイル・行を分別する仕組み 1. 「測定対象ファイル」のリストを作っておいて 2. pcov\collect()実行時に、 「測定対象ファイル」の内、測定対象行を分析 3. 事前に記録しておいた実行済みのファイル・行のデータと 統合して、未実行・実行済みの行を整理 96

Slide 97

Slide 97 text

まずはネタバレ 実行済み・未実行のファイル・行を分別する仕組み 1. 「測定対象ファイル」のリストを作っておいて 2. pcov\collect()実行時に、 「測定対象ファイル」の内、測定対象行を分析 3. 事前に記録しておいた実行済みのファイル・行のデータと 統合して、未実行・実行済みの行を整理 97

Slide 98

Slide 98 text

順番に見ていきましょう 1. 「測定対象ファイル」のリストを作る 2. pcov\collect()実行時: 測定対象行を分析 3. pcov\collect()実行時: 未実行・実行済みの行を整理 98

Slide 99

Slide 99 text

順番に見ていきましょう 1. 「測定対象ファイル」のリストを作る 2. pcov\collect()実行時: 測定対象行を分析 3. pcov\collect()実行時: 未実行・実行済みの行を整理 99

Slide 100

Slide 100 text

PHPスクリプトが実行されるまで 100 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM OpCode 機械語 Zend Compiler さっきの図

Slide 101

Slide 101 text

PHPスクリプトが実行されるまで 101 PHPのコード (いつもの) 本当のCPU 仮想マシン ZendVM OpCode 機械語 Zend Compiler さっきの図 ここでの仕事が大事

Slide 102

Slide 102 text

pcovのオーバーライド PHP_RINIT_FUNCTION(pcov) { /** লུ */ if (!zend_compile_file_function) { zend_compile_file_function = zend_compile_file; zend_compile_file = php_pcov_compile_file; } /** লུ */ return SUCCESS; }  pcov.c

Slide 103

Slide 103 text

pcovのオーバーライド PHP_RINIT_FUNCTION(pcov) { /** লུ */ /** লུ */ }  pcov.c zend_compile_file = php_pcov_compile_file; phpスクリプト→opcode変換処理の オーバーライド

Slide 104

Slide 104 text

php_pcov_compile_file (っぽいもの) function pcovCompileFile(FileHandler $file, int $type) { $op_array = zendCompilerඪ४ͷॲཧ($file, $type); if (!$op_array || !$op_array->filename || !ଌఆର৅ʹؚΊ͍͍͔ͯ($op_array->finename) || طʹ௥ՃࡁΈ͔($op_array->finename) ) { return $op_array; } ଌఆର৅ϑΝΠϧʹ௥Ճ($op_array->filename); /** GCपΓͷॲཧ͕ίίʹೖ͍ͬͯΔ */ return $op_array; } 

Slide 105

Slide 105 text

php_pcov_compile_file (っぽいもの) function pcovCompileFile(FileHandler $file, int $type) { $op_array = zendCompilerඪ४ͷॲཧ($file, $type); if (!$op_array || !$op_array->filename || !ଌఆର৅ʹؚΊ͍͍͔ͯ($op_array->filename) || طʹ௥ՃࡁΈ͔($op_array->finename) ) { return $op_array; } ଌఆର৅ϑΝΠϧʹ௥Ճ($op_array->filename); /** GCपΓͷॲཧ͕ίίʹೖ͍ͬͯΔ */ return $op_array; }  $op_array = zendCompilerඪ४ͷॲཧ($file, $type); 元の処理を実行して、 この関数の戻り値を取得する このデータ(op_array)が そのまま関数全体の戻り値になる

Slide 106

Slide 106 text

php_pcov_compile_file (っぽいもの) function pcovCompileFile(FileHandler $file, int $type) { $op_array = zendCompilerඪ४ͷॲཧ($file, $type); if (!$op_array || !$op_array->filename || !ଌఆର৅ʹؚΊ͍͍͔ͯ($op_array->finename) || طʹ௥ՃࡁΈ͔($op_array->finename) ) { return $op_array; } ଌఆର৅ϑΝΠϧʹ௥Ճ($op_array->filename); /** GCपΓͷॲཧ͕ίίʹೖ͍ͬͯΔ */ return $op_array; }  !ଌఆର৅ʹؚΊ͍͍͔ͯ($op_array->finename) 測定/除外対象の 設定内容との照合

Slide 107

Slide 107 text

php_pcov_compile_file (っぽいもの) function pcovCompileFile(FileHandler $file, int $type) { $op_array = zendCompilerඪ४ͷॲཧ($file, $type); if (!$op_array || !$op_array->filename || !ଌఆର৅ʹؚΊ͍͍͔ͯ($op_array->finename) || طʹ௥ՃࡁΈ͔($op_array->finename) ) { return $op_array; } ଌఆର৅ϑΝΠϧʹ௥Ճ($op_array->filename); /** GCपΓͷॲཧ͕ίίʹೖ͍ͬͯΔ */ return $op_array; }  طʹ௥ՃࡁΈ͔($op_array->finename) もうメモしてあるファイルだったらスキップ

Slide 108

Slide 108 text

php_pcov_compile_file (っぽいもの) function pcovCompileFile(FileHandler $file, int $type) { $op_array = zendCompilerඪ४ͷॲཧ($file, $type); if (!$op_array || !$op_array->filename || !ଌఆର৅ʹؚΊ͍͍͔ͯ($op_array->finename) || طʹ௥ՃࡁΈ͔($op_array->finename) ) { return $op_array; } ଌఆର৅ϑΝΠϧʹ௥Ճ($op_array->filename); /** GCपΓͷॲཧ͕ίίʹೖ͍ͬͯΔ */ return $op_array; }  ଌఆର৅ϑΝΠϧʹ௥Ճ($op_array->filename); ココが肝!!

Slide 109

Slide 109 text

順番に見ていきましょう 1. 「測定対象ファイル」のリストを作る 2. pcov\collect()実行時: 測定対象行を分析 3. pcov\collect()実行時: 未実行・実行済みの行を整理 109

Slide 110

Slide 110 text

測定対象行 #とは • 文や式が存在する、PHPが何かを実行する行 (雰囲気説明) • 「関数やクラスを宣言する行」は 含まれない • 空白行とか、`{}` があるだけの行も 含まれない • カバレッジの「分母」になる 110

Slide 111

Slide 111 text

\pcov\collect() のおさらい • 「実行された行」「されなかった行」のデータを返す関数 • この関数が呼ばれると、集めたデータの統合と整理を行う • コンパイル時に集めた 「対象ファイル」の一覧 • コード実行時に記録された 「実行された行」の一覧 111 // var_dump(\pcov\collect()); ["/opt/hoge.php"]=> array(4) { [13]=> int(-1) [7]=> int(1) [8]=> int(-1) [11]=> int(1) }

Slide 112

Slide 112 text

測定対象行の洗い出し: ここからやること 1. 分析対象ファイルを拾ってきて 2. op_arrayから制御フローグラフを構築して 3. 到達可能なブロックを拾ってきて 4. ブロックごとに、opcode種類(※命令)をみて拾う・捨てる 5. 残った全ての行を「未実行」としてマークする 112

Slide 113

Slide 113 text

そろそろコードばっか見せられて 疲れてきちゃったな、って思っていますか? 図で説明して良いですかね 113

Slide 114

Slide 114 text

測定対象行の分析の流れ 114 op_array 制御フローグラフ 結果データの初期化 コンパイル ZEND_APIの機能 測定対象行の抽出 カバレッジデータとの統合 PHPスクリプト

Slide 115

Slide 115 text

測定対象行の〜 115 op_array 制御フローグラフ 結果データの初期化 コンパイル ZEND_APIの機能 測定対象行の抽出 カバレッジデータとの統合 PHPスクリプト

Slide 116

Slide 116 text

測定対象行の〜 制御フローグラフ 結果データの初期化 ZEND_APIの機能 測定対象行の抽出 カバレッジデータとの統合 PHPスクリプト op_array コンパイル [000] L6 ZEND_INIT_FCALL op1=JMP->[002] op2 [001] L6 ZEND_DO_ICALL [002] L7 ZEND_INIT_FCALL op1=JMP->[006] op2 [003] L7 ZEND_SEND_VAL op1=CONST(1) op2=J [004] L7 ZEND_SEND_VAL op1=CONST(2) op2=J [005] L7 ZEND_DO_FCALL [006] L7 ZEND_ASSIGN op1=CV($x) op2=VAR [007] L8 ZEND_JMPZ op1=CONST(false) o [008] L9 ZEND_RETURN op1=CONST(null) [009] L10 ZEND_INIT_FCALL op1=JMP->[012] op2 [010] L10 ZEND_SEND_VAL op1=CONST("ίίʹ͸དྷ [011] L10 ZEND_DO_ICALL [012] L8 ZEND_JMP op1=JMP->[016] [013] L12 ZEND_INIT_FCALL op1=JMP->[016] op2 [014] L12 ZEND_SEND_VAL op1=CONST("ίίʹ͸དྷ [015] L12 ZEND_DO_ICALL [016] L14 ZEND_ECHO op1=CV($x) [017] L15 ZEND_INIT_FCALL op1=JMP->[019] op2 [018] L15 ZEND_DO_ICALL [019] L18 ZEND_INIT_FCALL op1=JMP->[021] op2 [020] L18 ZEND_DO_ICALL [021] L18 ZEND_ASSIGN op1=CV($coverage)

Slide 117

Slide 117 text

実行に関わりが無い行が消える op_arrayの時点で、 「実行可能なコードがない」行は消えている 117 [013] L12 ZEND_INIT_FCALL [014] L12 ZEND_SEND_VAL [015] L12 ZEND_DO_ICALL [016] L14 ZEND_ECHO [017] L15 ZEND_INIT_FCALL 011 012 013 014 015 } else { var_dump(' } echo $x; \pcov\stop(); L13は処理を持たない

Slide 118

Slide 118 text

測定対象行の〜 118 op_array 結果データの初期化 コンパイル 測定対象行の抽出 カバレッジデータとの統合 PHPスクリプト 制御フローグラフ ZEND_APIの機能 Block 0 (start=0 len=8) => REACHABLE Block 1 (start=8 len=1) => REACHABLE Block 2 (start=9 len=4) => UNREACHABLE Block 3 (start=13 len=3) => REACHABLE Block 4 (start=16 len=8) => REACHABLE

Slide 119

Slide 119 text

制御フローグラフ? • プログラムが通りうる経路を整理したやつ • 「ブロック」と、それらをつなぐ「ノード」で成り立つ • pcovでは、ブロックごとの「到達可能・不可能」を気にする 119

Slide 120

Slide 120 text

到達不能な行(ブロック)が区別される • op_arrayを分岐地点でブロックに区切る • 条件分岐やreturnがあるところが境目 120 006 007 008 009 010 011 012 013 014 \pcov\start(); $x = add(1, 2); if (false) { return; var_dump(' } else { var_dump(' } echo $x; Block 0 [REACHABLE] start=0 len=8 000 L6 ZEND_INIT_FCALL op1=JMP->002 001 L6 ZEND_DO_ICALL 002 L7 ZEND_INIT_FCALL op1=JMP->[00 // ... 006 L7 ZEND_ASSIGN op1=CV($x) o 007 L8 ZEND_JMPZ op1=CONST(fa

Slide 121

Slide 121 text

到達不能な行(ブロック)が区別される • op_arrayを分岐地点でブロックに区切る • 条件分岐やreturnがあるところが境目 121 006 007 008 009 010 011 012 013 014 \pcov\start(); $x = add(1, 2); if (false) { return; var_dump(' } else { var_dump(' } echo $x; Block 1 [REACHABLE] start=8 len=1 008 L9 ZEND_RETURN op1=CONST(null)

Slide 122

Slide 122 text

到達不能な行(ブロック)が区別される • op_arrayを分岐地点でブロックに区切る • 条件分岐やreturnがあるところが境目 122 006 007 008 009 010 011 012 013 014 \pcov\start(); $x = add(1, 2); if (false) { return; var_dump(' } else { var_dump(' } echo $x; Block 2 [UNREACHABLE] start=9 len=4 009 L10 ZEND_INIT_FCALL op1=JMP->[012] 010 L10 ZEND_SEND_VAL op1=CONST(" 011 L10 ZEND_DO_ICALL 012 L8 ZEND_JMP op1=JMP->[016

Slide 123

Slide 123 text

到達不能な行(ブロック)が区別される • pcovでは、「絶対に到達することのない」行は カバレッジの分母から消される • PHPスクリプトの10行目とはここでさようなら〜 123 008 009 010 if (false) { return; var_dump(' Block 2 [UNREACHABLE] start=9 len=4 009 L10 ZEND_INIT_FCALL op1=JMP->[012] 010 L10 ZEND_SEND_VAL op1=CONST(" 011 L10 ZEND_DO_ICALL 012 L8 ZEND_JMP op1=JMP->[016

Slide 124

Slide 124 text

測定対象行の〜 124 op_array 制御フローグラフ コンパイル ZEND_APIの機能 測定対象行の抽出 PHPスクリプト 結果データの初期化 カバレッジデータとの統合 /opt/pcov-demo.php: line 6:-1 line 7:-1 line 8:-1 line 9:-1 line 12: -1 line 14: -1 line 15: -1 line 18: -1 line 20: -1 line 23: -1 line 24: -1 line 25: -1 line 27: -1 全ての対象行に対する、 「行番号」と「結果」を 持つデータを構築 この時点では、 「-1 = 未実行」で 埋めておく

Slide 125

Slide 125 text

順番に見ていきましょう 1. 「測定対象ファイル」のリストを作る 2. pcov\collect()実行時: 測定対象行を分析 3. pcov\collect()実行時: 未実行・実行済みの行を整理 125

Slide 126

Slide 126 text

測定対象行の〜 126 /opt/pcov-demo.php: line 6:-1 line 7: 1 line 8: 1 line 9:-1 line 12: 1 line 14: 1 line 15: 1 line 18: -1 line 20: -1 line 23: -1 line 24: -1 line 25: -1 line 27: -1 最終的な測定結果 結果データの初期化 測定対象行の抽出 カバレッジデータとの統合 PHP側に結果を返す

Slide 127

Slide 127 text

実行済みの行を、どうやってマークするの? ここまで来たら あとはメッチャ単純です!(やったね) 127

Slide 128

Slide 128 text

カバレッジの測定データを組み立てる • 測定対象行(実行可能行)・実行された行のデータが揃った 128

Slide 129

Slide 129 text

カバレッジの測定データを組み立てる • 測定対象行(実行可能行)・実行された行のデータが揃った • これらを突き合わせるだけ! • 実行された行のデータ(のコレクション)を使って • まだ実行済みとしてマークされていなかったら、-1 => 1に 129

Slide 130

Slide 130 text

測定対象行の〜 130 /opt/pcov-demo.php: line 6:-1 line 7: 1 line 8: 1 line 9:-1 line 12: 1 line 14: 1 line 15: 1 line 18: -1 line 20: -1 line 23: -1 line 24: -1 line 25: -1 line 27: -1 最終的な測定結果 結果データの初期化 測定対象行の抽出 カバレッジデータとの統合 PHP側に結果を返す

Slide 131

Slide 131 text

① pcovの基本的な使い方や機能を見てみる ② 実行されたファイル・行を記録する仕組み ③ 実行済み・未実行のファイル・行を分別する仕組み ④ まとめ

Slide 132

Slide 132 text

リマインド: 今日のお話 カバレッジ測定を通じて、 「PHP拡張は、どんな機能を提供しているのか」 「何を可能にしているのか」 を知っていきましょう 132

Slide 133

Slide 133 text

PHP拡張は、どんな機能を提供しているのか • 拡張を開発するには、PHP実行の主要なライフサイクルに そのPHP拡張の独自処理を差し込めるフックを利用する • ex: PHP_MINIT_FUNCTION / PHP_RINIT_FUNCTION 133

Slide 134

Slide 134 text

何を可能にしているのか • 通常の処理をラップして、独自の仕事を付加している • ex: zend_execute_ex のオーバーライド • oplineの処理時に、「これがどこの行だったか」を記録 134

Slide 135

Slide 135 text

何を可能にしているのか • 通常の処理をラップして、独自の仕事を付加している • ex: zend_execute_ex のオーバーライド • oplineの処理時に、「これがどこの行だったか」を記録 • 独自関数内部で、ZendVMのAPIも利用してデータ作成 • ex: \pcov\collect() 内部での、zend_build_cfgの利用。 制御フローグラフの構築と、到達不可能ブロックの洗い出し 135

Slide 136

Slide 136 text

PHP拡張ってこんな感じ PHPの基本機能を乗っ取ったり 処理を追加するための 仕組みがあった! 136

Slide 137

Slide 137 text

終わっての感想

Slide 138

Slide 138 text

お疲れ様でした! 「コードカバレッジ」が 「何をカバーしている」のか、「どうやっている」のか、 少しでも感じられましたか? 138

Slide 139

Slide 139 text

お疲れ様でした! ここまで知らなくても、 困ることは無いのですが 知ってみると気持ち良い(きっと) 139

Slide 140

Slide 140 text

お疲れ様でした! 最高に気持ちよくなりたい? JetBrainsの「CLion」、 めっちゃ便利なので使っていきましょう 140

Slide 141

Slide 141 text

お疲れ様でした! 最高に気持ちよくなりたい? JetBrainsの「CLion」、 めっちゃ便利なので使っていきましょう 141 今すぐブースへ走 れ

Slide 142

Slide 142 text

pcovは、コードサイズも小さいし エクステンション開発の最初の一歩に いかがでしょうか? 142

Slide 143

Slide 143 text

pcovは コードサイズも 小さいし 最初の一歩に いかがでしょうか? 143

Slide 144

Slide 144 text

おしまい! お付き合いいただき ありがとうございました!!

Slide 145

Slide 145 text

Appendix Ⅰ カバレッジデータの利用に関する資料

Slide 146

Slide 146 text

PHPUnitと連携したコードカバレッジ取得 • PHPerKaigi 2024で発表したものがあります • Xdebug中心の紹介ですが、仕組みは同じ 146 https://speakerdeck.com/o0h/phperkaigi-2024

Slide 147

Slide 147 text

OpCodeとカバレッジ • 今回の発表で(ほとんど)省略している、 「OpCodeレベルで見て分かること」についての言及 147 https://speakerdeck.com/o0h/phperkaigi-2024-omake

Slide 148

Slide 148 text

Appendix Ⅱ PHP拡張の開発と内部処理に関する資料

Slide 149

Slide 149 text

PHP拡張の中身・開発 • Writing PHP Extensions - YouTube • https://www.youtube.com/playlist?list=PLg9Kjjye- m1hW4z0J-546qaFpysjlo27x • Xdebug作者のDerickさんのYoutube • Writing PHP Extensions | Zend • https://www.zend.com/resources/writing-php-extensions • 古いかつ英語ですが、基本的なコンセプトは通じるはず 149

Slide 150

Slide 150 text

最近のコミュニティでのPHP拡張関連の情報 • Deep Dive into Xdebug by nsfisis • @PHPカンファレンス小田原2026 • https://fortee.jp/phpconodawara-2026/proposal/ 2ff16827-3893-480e-b73b-0ff88e65e555 • PHP7.4でもOpenTelemetryゼロコード計装がしたい! by Arthur • @PHPerKaigi 2026 • https://fortee.jp/phperkaigi-2026/proposal/8310bab7- a3d3-4b7a-8c98-8d6a97c4fc00 150

Slide 151

Slide 151 text

PHP(内部)入門的なもの • php-src 入門: ぼくらの画面に hello world が届くまで sji • @PHPカンファレンス福岡2024 • https://www.youtube.com/watch?v=fpRcZiTi5As 151

Slide 152

Slide 152 text

PHPの開発 • Using CLion with php-src - DEV Community • https://dev.to/ramsey/using-clion-with-php-src-4me0 • 少し前の記事だが、基本的にこの通りにやればphp-srcからPHPのデ バッグ実行まで行けるようになる(簡単!) • エラーになったら、ログやスクショを生成AIさんに渡して乗り切る 152

Slide 153

Slide 153 text

Appendix Ⅲ PHPの実行の様子 - oplineの逐次処理まで

Slide 154

Slide 154 text

php_execute_script〜php_pcov_trace までの流れを、ステップ実行したキャプチャ 154

Slide 155

Slide 155 text

PHPの実行の様子 - oplineの逐次処理まで 155

Slide 156

Slide 156 text

PHPの実行の様子 - oplineの逐次処理まで 156

Slide 157

Slide 157 text

PHPの実行の様子 - oplineの逐次処理まで 157

Slide 158

Slide 158 text

PHPの実行の様子 - oplineの逐次処理まで 158

Slide 159

Slide 159 text

PHPの実行の様子 - oplineの逐次処理まで 159

Slide 160

Slide 160 text

PHPの実行の様子 - oplineの逐次処理まで 160

Slide 161

Slide 161 text

PHPの実行の様子 - oplineの逐次処理まで 161