Share
Embed
PHP8.5は「戻り値の無視」をどう見抜くのか 〜voidキャストの裏側〜
PHP8.5から実装されたNoDiscardの実装試用の裏側に迫ります
Shinya Saita
September 27, 2026
Transcript
20 slides
-
PHP8.5は「戻り値の無視」をどう見抜くのか #[\NoDiscard] と (void) キャストの裏側 実際のソースコードを読んで解明 〜OPCode と Zend VM
の挙動〜 関西PHP勉強会 / 2026-08-20 斉田真也(X: @saita̲shinya / GitHub: shinya) -
自己紹介 斉田 真也(さいた しんや) Webエンジニア 株式会社ラクスのバックエンド 個人では Markdownエディタ Bokuchi を
開発中(OSS) 趣味: ものづくり全般 特技: カードマジック 各種アカウント X: @saita̲shinya GitHub: shinya Qiita: @ssaita 2 -
作ってるもの Bokuchi 自作のMarkdownエディタ(OSS) Tauri + React + Rust 製 (PHPではない・・・)
https://bokuchi.com/ 3 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 PHP 8.4までの以下のソースコードの挙動 $deadline
= new DateTimeImmutable('2026-08-20'); $deadline->modify('+7 day'); // 締切を1週間延ばした……つもり echo $deadline->format('Y-m-d'); 結果は 2026-08-20 のまま。 DateTimeImmutable は自分自身を書き換えないので、新しいオブジェクトを返す挙動。 2行目の戻り値を受け取り忘れるのは陥りやすいバグ PHP 8.4 までは……特にエラーも何も出ないし、検知もしない 斉田真也 / X: @saita̲shinya / GitHub: shinya 6 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 PHP 8.5 にアップグレードすると・・・
Warning: The return value of method DateTimeImmutable::modify() should either be used or intentionally ignored by casting it as (void), as DateTimeImmutable::modify() does not modify the object itself 「戻り値を使うか、 (void) を付けてわざと無視だとしてる宣言をしよう」 と警告をしてくる。 これは PHP 8.5 で新しく搭載された #[\NoDiscard] 属性にあたる。 RFC: marking̲return̲value̲as̲important((void) キャストも同 RFC で追加) #[\NoDiscard("as DateTimeImmutable::modify() does not modify the object itself")] public function modify(string $modifier): DateTimeImmutable {} ※ 標準の 関数にも採用済みだし、もちろん自作の関数・メソッドにも付けられる 斉田真也 / X: @saita̲shinya / GitHub: shinya 7 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 じゃあどんな風に使うのか? #[\NoDiscard("Be sure
to use the returned value.")] function returnFalse(): bool { return false; } returnFalse(); $ok = returnFalse(); (void) returnFalse(); // // // Warning が出る 使っているので出ない 「わざと捨てた」の意思表示。出ない #[\NoDiscard] … 関数・メソッドに付与。戻り値が捨てられたら E̲WARNING が出る (void) キャスト … 8.5 で追加された新しいキャスト。 警告を黙殺する公式な方法 当たり前ですが、マニュアルにも書かれています。 斉田真也 / X: @saita̲shinya / GitHub: shinya 8 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 本題 PHPのエンジンはどうやって 「戻り値が捨てられた」ことを知るのか?
A. 構文木(AST)に「捨てた」フラグが記録される B. 実行時に値の参照カウントを監視する C. もっとシンプルな何か 斉田真也 / X: @saita̲shinya / GitHub: shinya 9 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 前提: PHP コードの一生
文字列 ソースコード → トークン列 ① Lexer → AST ② Parser → OPCode ③ Compiler → 実行 ④ Zend VM PHP は 上記のフローを通ってZend VM が実行する(らしい) この OPCode、実は簡単に見ることが出来る php -d zend_extension=opcache -d opcache.enable_cli=1 \ -d opcache.opt_debug_level=0x10000 demo.php 斉田真也 / X: @saita̲shinya / GitHub: shinya 10 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 OPCode ダンプの見方 $ok
= validate(); これをダンプすると、この3命令になる: 0005 INIT_FCALL 0 80 string("validate") 0006 V3 = DO_FCALL_BY_NAME 0007 ASSIGN CV0($ok) V3 記号 ; 呼び出しの準備 ; 実行して、結果を V3 に入れる ; V3 を $ok に代入 意味 V3 テンポラリ変数(命令の結果の一時置き場) CV0($ok) コンパイル済み変数 = PHP の $ok V3 = の部分 結果オペランド(結果をどこに置くか) 斉田真也 / X: @saita̲shinya / GitHub: shinya 11 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 実験: 3通りの「呼び方」を並べてみる validate();
(void) validate(); $ok = validate(); // ① 捨てる // ② わざと捨てる // ③ 使う 0000 INIT_FCALL 0 80 string("validate") 0001 DO_FCALL_BY_NAME ; ① 結果オペランドが無い! 0002 INIT_FCALL 0 80 string("validate") 0003 V2 = DO_FCALL_BY_NAME ; ② 結果を V2 に受け取って…… 0004 FREE V2 ; 直後に捨てている! 0005 INIT_FCALL 0 80 string("validate") 0006 V3 = DO_FCALL_BY_NAME ; ③ 結果を V3 に受け取って…… 0007 ASSIGN CV0($ok) V3 ; $ok に代入 ①だけ Vほげ = が無い。②はなんと一度受け取ってから捨てている(詐欺やんけ) 斉田真也 / X: @saita̲shinya / GitHub: shinya 12 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 「捨てた」というような情報自体は、どこにも無い エンジンに「戻り値が捨てられた」を表す専用データは存在しない あるのは、CALL
命令の結果オペランドが UNUSED かどうか VM は実行時に「この命令、結果の置き場が無い」を見て警告を出す そして (void) 使ったキャストの中身は 結果を一度受け取って、即 FREE する命令列をわざと出させることで、 VM に「使った」と思わせる、ある意味事実だけ残すような形 答えは C. もっとシンプルな何か でした。 斉田真也 / X: @saita̲shinya / GitHub: shinya 13 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 Cのソースコードで確認①: 捨て方どう実装しているか foo();
と書くと、コンパイラの zend_do_free() が後始末を担当。 FREE 命令を出す……のではなく、CALL 命令の結果欄そのものを消す: /* Zend/zend_compile.c: zend_do_free() */ if (opline->result_type == IS_VAR && opline->result.var == op1->u.op.var) { ... SET_UNUSED(opline->result); /* 結果オペランドを UNUSED に */ } 直前に出した CALL 命令を振り返って、結果オペランドを UNUSED に書き換える 「捨てた」という現象は、命令の結果欄が空欄になっていること 斉田真也 / X: @saita̲shinya / GitHub: shinya 14 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 Cのソースコードで確認②: (void) はどうなってるか
(void) 専用のコンパイル関数がある。UNUSED 化を あえてせず、FREE を発行: /* Zend/zend_compile.c: zend_compile_void_cast() */ case IS_TMP_VAR: case IS_VAR: opline = zend_emit_op(NULL, ZEND_FREE, &expr_node, NULL); opline->extended_value = ZEND_FREE_VOID_CAST; break; 結果オペランドは残る(= VM から見れば「使った」となる)→ 警告が出ない ZEND_FREE_VOID_CAST の印は、後でオプティマイザが使う(→ 後述のオチ) ちなみに、 (void) は式ではなく「文」 文法レベルで文の先頭にしか書けないので $x = (void) f(); は構文エラーになる (Zend/zend̲language̲parser.y) 斉田真也 / X: @saita̲shinya / GitHub: shinya 15 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 Cのソースコードで確認③: 警告を出す VM
側 すべての関数呼び出しが通る VM ハンドラの冒頭(Zend/zend̲vm̲def.h): const uint32_t no_discard = RETURN_VALUE_USED(opline) ? 0 : ZEND_ACC_NODISCARD; if (UNEXPECTED(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED | no_discard))) { ... zend_nodiscard_function(fbc); /* ここで Warning を発報 */ } #[\NoDiscard] の実体は関数フラグの 1ビット RETURN_VALUE_USED() = 「結果オペランドが UNUSED でないか」を見るだけ Deprecated チェックと同じ1回のビット演算に相乗り → 普通の関数の追加コストはゼロ 警告文は、属性オブジェクトを実際に生成して $message を取り出して合成している 斉田真也 / X: @saita̲shinya / GitHub: shinya 16 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 実は・・・opcache オプティマイザの「気配り」 さっきの3行に
opcache の最適化をかけると( opt_debug_level=0x20000 ): ; 最適化前 0001 DO_FCALL_BY_NAME 0003 V2 = DO_FCALL_BY_NAME 0004 FREE V2 0006 V3 = DO_FCALL_BY_NAME 0007 ASSIGN CV0($ok) V3 ; 最適化後 0001 DO_FCALL_BY_NAME (②は呼び出しごと消滅!) ← ①は温存 0002 ASSIGN CV0($ok) bool(false) ↑ ③は定数畳み込み (void) した②は「捨てると宣言済み」なので、呼び出しごと削除してよい なかったことになる(!?) 警告対象の①は、消すと警告まで消えてしまうので、最適化を拒否して温存 ここが個人的に「ほぉーん」となった ※言語仕様が、ちゃんとオプティマイザで配慮されてるのは( ・ ・)イイ!! 斉田真也 / X: @saita̲shinya / GitHub: shinya 17 -
関西PHP勉強会 / PHP 8.5 は「戻り値の無視」をどう見抜くのか / 2026-08-20 まとめ 「戻り値を使ったか」の判定は、OPCode の結果オペランドが
UNUSED か で判断 foo(); → コンパイラが CALL 命令の結果欄を消す( zend_do_free ) (void) foo(); → 受け取って即 FREE する命令を出し、VM に「使った」と思わせる VM は Deprecated チェックと相乗りのビット演算1回で警告を判定 opcache は (void) 済みの呼び出しは消し、警告対象は温存する php-src(Cのソースコード)は grep NODISCARD するとすぐに見つけられます。 興味があれば読んでみてください 斉田真也 / X: @saita̲shinya / GitHub: shinya 18