aboutsummaryrefslogtreecommitdiffhomepage
path: root/slide.md
blob: 96a5aeaffb5a57caa1506f85587385fdec80dd8e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
詳説「参照」、PHP の参照を完全に理解する、というタイトルで発表いたします。

--------------

いまむらと申します。
普段はデジタルサーカス株式会社で PHP を書いています。

--------------

本発表のアジェンダがこちらになります。

まず始めに、PHP の参照に関するクイズを出題いたします。
次に、それらの問題を解説するために必要な知識として、PHP 処理系のソースコードを読んでいきます。
最後に、その知識に基づいて、クイズの解説をします。

では早速、参照の不思議クイズと参りましょう。

--------------

このクイズでは、短い PHP スクリプトを表示しますので、出力される値を答えてください。
シンキングタイムは、10 秒程度取ります。

では、第一問。

--------------

x に 1 を代入して、
y に x を参照で代入しています。
そのあと、y に 42 を代入すると、x と y はどうなるでしょうか。

(10 秒待つ)

--------------

はい、正解を発表します。
x も y も、42 になります。
これが、一番オーソドックスな参照の動きになるかと思います。
x と y は同じものを指しているので、y に代入すると、x にも波及する、と。

では、第二問。

--------------

先ほどと同じく、x に 1 を代入して、
y に x を参照で代入しています。
そのあと、z に y を参照でなく、普通に代入しています。
z に 42 を代入すると、x、y、z はどうなるでしょうか。

(10 秒待つ)

--------------

はい、正解を発表します。
42 になるのは z だけで、x と y は 1 のままです。
ここからわかることは、y が参照であったとしても、z に代入するときに普通の代入をおこなうと、それは参照ではなくただのコピーになる、ということですね。

では、第三問

--------------

xs に、1 と 2 からなる配列を代入して、
x には、xs の 0 番目を参照で代入します。
そのあと、x に 42 を代入すると、x と xs はそれぞれどのようになるでしょうか。

(10 秒待つ)

--------------

はい、正解を発表します。
x と、xs の 0 番目が 42 に変わり、xs の 1 番目は、2 のままです。
こちらは、参照を取るのが配列の要素になっただけで、動きとしては、第一問とおおよそ同じです。

では、第四問、これが最後です。

--------------

xs に、1 と 2 からなる配列を代入して、
x には、xs の 0 番目を参照で代入します。
そのあと、ys に、xs を参照でなく、普通に代入します。
x に 42 を代入して、ys の 1 番目に 3 を代入すると、
x、xs、ys は、それぞれどうなるでしょうか。

(10 秒待つ)

--------------

はい、正解を発表します。
x と xs の 0 番目が 42 になるのはいいんですが、ys の 0 番目も 42 になっています。xs の 1番目と ys の 1番目が異なっていることから、xs と ys そのものは、異なる配列を指しています。しかしながら、xs の 0 番目を指している参照が存在すると、普通の代入でコピーされたはずの ys の 0 番目まで、参照になってしまう、ということです。

ここまでは、PHP レベルでの参照の挙動を見てきましたが、

--------------

ここからは、C 言語で書かれた、PHP の処理系のソースコードを読んでいきます。それによって、ここまでのクイズで何が起きていたのかを説明します。

--------------

具体的な実装を読んでいく前に、いくつか、注意点があります。このスライドで紹介するソースコードは、PHP の 8.2.3 のものです。説明の都合上、ソースコードはそのままの引用ではなく、改変をおこなっています。掲載しているソースコード片には、PHP 処理系のライセンスが適用されます。

また、これは C 言語を読み書きできる方向けですが、本発表は、C 言語を事前知識として仮定しないように、制作したつもりです。その関係上、C 言語としては、不正確な説明を、したり載せたりすることがあります。
例えば、この発表では、ポインタの話はしません。ポインタ関連の話がでてきそうになったら、適当にごまかして先に進みます。ほかにもいくつか、嘘を言っていることがありますが、このスライドの最後に、そういったところが気になった方向けの補足を置いています。

では、ソースコードを読んでいきましょう。

--------------

まずは、zval と zend_reference の話をしようと思います。

--------------

zval というのは、PHP のありとあらゆる値を表すためのデータ構造です。PHP の値というのは、例えば、整数、浮動小数点数、文字列、配列、クラスなどのことです。こういった値はすべて、内部的には zval で表されています。

では、zval がどんなデータ構造なのか、ソースを見ていきましょう。

--------------

zval の定義は、このようになっています。
まず、1行目に struct とありますが、これは、PHP でいう class とおおよそ同じです。その下に並んでいるのはメンバ変数ですが、PHP と同じで、型名が前にきて、後ろに変数名がくるようになっています。
まず 1つ目は、zend_value 型の value という変数、2つ目は、uint32_t 型の type_info という変数です。
zend_value 型については、このあとすぐ見ていきます。uint32_t というのは、32 bit からなる符号なし整数型です。PHP だと、int 型だと思ってください。この2つはそれぞれ、値の本体と、値の型情報を保持しています。
もう1つ、u2 というメンバ変数もあるんですが、これは今回使わないので、説明は省略します。

では、zend_value が何なのかを見ていきましょう。

--------------

zend_value は、union です。union は、PHP でも最近使えるようになりましたが、ここに並んでいる型の、どれか 1つが入っている、というような型です。zval というのは、PHP のありとあらゆる値を保持するデータ構造だったので、それらのうちのどれがきても保持できるような構造になっています。
具体的に見てみると、整数の lval、浮動小数点数の dval、文字列、配列、オブジェクト、リソースときて、最後に、zend_refenrece 型の参照がきています。zend_value には、これらのうちのいずれかが入っています。

--------------

ただ、zend_value には、これらのうち、いったいどれが入っているのか、を区別する情報はありません。したがって、何らかの手段を用いて、どの値が実際に格納されているのかを知る必要があります。

--------------

zval の定義に戻ります。ここで、zval の 2つ目のメンバ変数を見ると、type_info となっています。これは、その名のとおり、型情報を保持している変数です。この値を見て、zend_value に何が入っているのかを区別しています。

この型情報に入っている、具体的な値を見てみましょう。

--------------

null に使う IS_NULL や整数に使う IS_LONG、文字列に使う IS_STRING などがありますが、一番下に、参照に対して使われる IS_REFERENCE というのがあります。
ここからわかるのは、参照というのは、内部的には int や float などと同じく、独立した型として実装されている、ということです。

--------------

では、zend_value の定義に戻ります。
一番下にある、zend_reference というのが、参照で使われるデータ構造です。
これの定義を見ていきましょう。

--------------

上から、refcount、type_info、val、sources となっています。type_info と sources については、今回は説明を省略します。refcount というのは、ガベージコレクションで使われる参照カウントです。参照カウントというのは、同じ値への参照がいくつあるのかを数えるものです。3つ目の zval 型の val ですが、これが、この参照の指す実際の値になっています。

PHP の値の内部表現を押さえたところで、具体例を見ていきましょう。

--------------

左のコードのように、x イコール 1 としたときの、x のデータ構造を右に図示しました。
x は zval で、その type_info は、IS_LONG、すなわち整数型になっています。zend_value 型の value には、1 が入っています。

--------------

では、参照が絡むとどうなるでしょうか。
x に 1 を代入したあと、y に x を参照で代入してみました。このときの x や y は、PHP 処理系の中で、どのように表現されているでしょうか。

これを理解するためには、データ構造だけでなく、参照代入の処理がどのようにおこなわれているかを見ていく必要があります。

--------------

参照で代入をおこなったときの処理は、zend_assign_to_variable_reference という関数に記載されています。

ここで一つ注意ですが、このコードは、これまでにも増して、かなりの改変をおこなっています。エラー処理であるとか、今回の例示コードで通らないようなパスは、完全に排除していますので、処理系のオリジナルを参照される際はご注意ください。

では、頭から見ていきます。
今回は、PHP で、右のようなソース、lhs イコールアンパサンドの rhs と書いたときの処理を追っていきます。左にある C のソースでも、変数名は揃えています。lhs というのは left hand side の略で、左辺という意味です。同じく、rhs は right hand の略で、右辺です。

左にある C 言語のソースを見ると、ZVAL_NEW_REF という処理を呼び出しているので、こちらについて見ていきます。

--------------

この関数は、端的に言うと、rhs の中身を参照でラップする、というものです。
具体的には、まず、PHP における参照の実体である、zend_reference 型の値を作成します。次に、その zend_refenrece の参照カウントを 1 にします。この時点では、この参照を指しているのは rhs 1 つですので、参照カウントも 1つと、いうことになります。続けて、rhs の値を、zend_reference の中に入っている zval へコピーします。最後に、rhs そのものを、作成した zend_reference 型の値で置き換えて、type_info も IS_REFERENCE にしてしまいます。
これによって、元々 rhs の表していた値が、参照を経由して指し示されるように変化しました。

--------------

では、元の関数に戻ります。

ZVAL_NEW_REF を呼んだあとは、rhs の refcount、参照カウントを 1 増やしています。これは、lhs も、rhs と同じ参照を指すようになるからです。最後に、lhs へ rhs の値を代入して、lhs の type_info を、IS_REFERENCE にしています。

--------------

それでは、先ほどの PHP のコードに戻ってみましょう。
rhs に 1 を代入して、lhs に rhs を参照で代入しています。

右に示した図のように、まずは、rhs が作成されます。type_info は IS_LONG 整数型で、value は 1 です。

--------------

ここで、ZVAL_NEW_REF が呼び出されます。

まずは、zend_refenrece 型の値を作成して、refcount を 1 にします。

--------------

次に、rhs の値を、zend_reference の中にある zval にコピーします。
つまり、zend_reference の中の zval が、type_info IS_LONG で、value 1 になります。

--------------

最後に、rhs の type_info を IS_REFERENCE にして、rhs が、作成した zend_reference を指すようにします。

これで、ZVAL_NEW_REF の処理が終わりです。

--------------

次におこなうのは、rhs が指す zend_reference の refcount を 1増やす処理です。refcount が 2 になりました。

--------------

最後に、lhs が、rhs と同じ zend_reference を指すようにして、type_info も IS_REFERENCE にします。こうして、右の図のようなデータ構造が作られます。

さて、準備が整ったので、冒頭のクイズに戻りたいと思います。

--------------

ではまず、第一問です。

x に 1 を代入して、
y に x を参照で代入しています。
これは、x も y も、42 になります。

--------------

このとき、PHP の処理系では、右の図のようなデータが作られています。つまり、y と x は同じ zend_reference を指していて、その zend_reference の中には、42 が入っている、と。x と y は同じ zend_reference を共有しているため、y に対してだけ代入をおこなっても、x にも反映されています。

--------------

続いて、第二問です。
x に 1 を代入して、y に x を参照で代入しています。
そのあと、z に y を普通に代入しています。
z に 42 を代入すると、z だけが 42 になって、x と y は 1 のままです。

このときの、内部の動きを見ていきましょう。

--------------

まず、y に x を参照で代入したところまでを図示したのがこちらです。ここまでは、先ほどとまったく同じです。

--------------

次に、z に y を代入します。本当は、参照でない普通の代入をしたときの PHP 処理系のソースコードも紹介すべきなんですが、そこまでは入り切らなかったので、ここで軽く紹介します。PHP で通常の代入をおこなうと、値がコピーされます。コピーするときにそれが参照だった場合は、参照の中に入っている値がコピーされます。

--------------

つまり、この図のように、y や x とは独立して、z は 1 という値になります。

--------------

ここから、z だけを 42 にするので、y や x には何ら影響を及ぼさない、というわけです。

--------------

続いて、第三問です。

xs に、1 と 2 からなる配列を代入して、
x には、xs の 0 番目を参照で代入します。
そのあと、x に 42 を代入すると、
x と、xs の 0 番目が 42 になります。

--------------

PHP の配列の内部構造については、詳しく立ち入りませんが、参照のときの zend_reference と同じように、配列には、zend_array という型があり、それがそれぞれの要素の zval を保持しています。
xs に配列を代入すると、右の図のようなデータが作られます。
xs が zend_array を指していて、zend_array は、それぞれ 1 と 2 を持っています。

--------------

ここで xs の 0 番目を x に代入すると、まず、ZVAL_NEW_REF の効果によって、xs の 0 番目が、参照を経由する形にラップされます。

--------------

そして、x は、その参照を指します。

--------------

最後に x へ 42 を代入すると、x と xs の 0 番目が、両方 42 になります。

--------------

では、第四問。
xs に、1 と 2 からなる配列を代入して、
x には、xs の 0 番目を参照で代入します。
そのあと、ys に、xs を参照でなく、普通に代入します。
x に 42 を代入して、ys の 1 番目に 3 を代入すると、

x と xs の 0 番目と、ys の 0 番目がすべて 42 になります。

--------------

まずは、xs の 0 番目を、x に参照で代入したところまでを見てみます。
これは、先ほどと、まったく同じですね。

次に、ys に xs を代入します。配列の代入について詳しく話すためには、本来 copy on write という仕組みについて説明する必要があるのですが、ここでは、話を簡単にして、単に、コピーされるということにします。

--------------

xs を ys にコピーしたのがこの図です。ポイントになるのは、ys の 1 番目は xs の 1 番目と共有されていないのに対して、ys の 0 番目は、xs の 0 番目と同じ zend_reference を指している、ということです。

--------------

この状態で、x に 42 を代入すると、xs の 0 番目のみならず、ys の 0 番目にも影響が波及します。

これで、クイズの解説は以上になります。

--------------

さて、今回、PHP の処理系のソースコードをごく一部紹介したわけですが、C 言語で書かれたソフトウェアというのは、Web のバックエンドだけに限っても、PHP や Apache httpd、MySQL など、いくつも存在します。C 言語を読むことができると、奇想天外なバグに出会ったときや、仕様が不明瞭な API を使うときなどに、ソースコードを参照する、という最も確実な手段を取ることができます。もちろん、マニュアルやドキュメントが整備されていれば、それを見ればいいんですが、ソースコードリーディングを選択肢として持っておけるのは、大きなメリットがあります。

PHP の処理系全体は 200 万行ほどある巨大なコードベースですが、今回のように、その一部を知るためだけなら、せいぜい 1000 行ほど読めば十分な情報を得ることができます。
みなさんも、PHP の関数や言語仕様でよくわからないことがあったときには、処理系のソースを読んでみてはいかがでしょうか。

以上で発表を終わりたいと思います。ご静聴、ありがとうございました。

--------------