값 변환
setGlobal, call, JS에서 호출되는 호스트 함수 등 PHP↔JS 경계를 통과하는 모든 호출은 변환기를 거칩니다:
- 호스트 값이 엔진으로 들어올 때의 PHP → JS (
setGlobal(),call()인수, 호스트 함수 반환값). - JS 값이 다시 나갈 때의 JS → PHP. 규칙이 약간 다른 두 가지 경로가 있습니다.
eval()및call()의 결과와 JS가 호스트 함수에 전달하는 인수입니다.
변환은 복사 방식입니다. 원시 값은 원시 값으로 매핑되고, 배열과 객체는 새 구조로 복사되며, PHP 호출 가능 객체는 JS 함수로 래핑됩니다. 호출 가능 객체 자체를 제외하면 참조로 공유되는 것은 없습니다.
PHP → JS
| PHP 값 | JS 결과 |
|---|---|
null |
null |
true / false |
true / false |
int |
number (IEEE 754 double; 2^53를 넘는 정수는 정밀도를 잃음) |
float |
number (NaN, ±Infinity, ±0 보존) |
string |
string (UTF-8 PHP 바이트를 UTF-16 코드 단위로 해석), PHP 함수 이름인 경우에도 해당 |
리스트 array (키 0, 1, 2, … 순서대로) |
새 Array, 요소를 재귀적으로 변환 |
그 밖의 모든 array |
새 일반 Object, 값을 재귀적으로 변환 |
Closure, 호출 가능한 객체, [$object, 'method']와 같은 호출 가능한 배열 |
function, PHP 호출 가능 객체를 호출 |
그 밖의 모든 객체, Generator, 리소스 |
undefined |
혼합 키 배열(PHP의 [0 => 'a', 'name' => 'b'])은 일반 객체가 됩니다. JS의 배열은 연속된 정수 인덱스 공간을 가져야 하기 때문입니다.
$engine->setGlobal('list', [1, 2, 3]); // → JS Array
$engine->setGlobal('map', ['a' => 1, 'b' => 2]); // → JS Object
$engine->setGlobal('mixed', [0 => 'x', 'k' => 'v']); // → JS Object
배열은 복사되므로 JS에서 변경해도 JS에 그대로 남습니다:
$list = [1, 2, 3];
$engine->setGlobal('list', $list);
$engine->eval('list.push(4)');
count($list); // 3
$engine->eval('list.length'); // 4
호출할 수 없는 PHP 객체는 변환되지 않고 undefined로 전달됩니다(이슈 #18). 먼저 get_object_vars() 등을 사용해 배열로 변환하거나 메서드를 클로저로 노출하세요. 공유 객체를 참조하세요.
JS → PHP: eval() 및 call() 결과
| JS 값 | PHP 결과 |
|---|---|
undefined, null |
null |
boolean |
bool |
number (정수이며 int 범위 내) |
int (-0는 0이 됨) |
NaN, Infinity, -Infinity |
NAN, INF, -INF |
number (기타) |
float |
string |
string (UTF-8) |
bigint, symbol |
null |
function |
null |
Array |
리스트 array, 요소를 재귀적으로 변환 |
TypedArray |
숫자로 이루어진 리스트 array |
그 밖의 모든 객체(일반 객체, Error, Date, Map, Set, Promise, ArrayBuffer, …) |
자체 데이터 속성을 담은 연관 array. 접근자와 함수 값 속성은 제외 |
마지막 행은 내부 슬롯에 상태를 저장하는 객체가 빈 배열로 변환된다는 뜻입니다. new Date(0), new Map([["a", 1]]), Promise.resolve(5)는 모두 []을 반환합니다. Error는 자체 message 및 stack을 반환합니다. 자기 자신을 참조하는 객체는 내부 참조를 null로 변환합니다.
$engine->eval('[1, 2, 3]'); // [1, 2, 3]
$engine->eval('({a: 1, b: 2})'); // ['a' => 1, 'b' => 2]
$engine->eval('new Uint8Array([1, 2, 3])'); // [1, 2, 3]
$engine->eval('1234567890123456789n'); // null
$engine->eval('new Date(0)'); // []
이러한 값 중 하나가 PHP에서 필요하면 JS 내부에서 변환하세요:
$engine->eval('(1234567890123456789n).toString()'); // "1234567890123456789"
$engine->eval('new Date(0).toISOString()'); // "1970-01-01T00:00:00.000Z"
$engine->eval('Object.fromEntries(new Map([["a", 1]]))'); // ['a' => 1]
JS → PHP: 호스트 함수 인수
JS가 PHP 호스트 함수에 전달하는 인수는 원시 값, 배열, 일반 객체에 대해 같은 규칙을 따르며 다음과 같은 차이가 있습니다:
| JS 값 | PHP 인수 |
|---|---|
TypedArray, ArrayBuffer, DataView |
변환되지 않은 엔진 자체의 JsValue 객체 |
| 객체 | 접근자(게터 실행)와 함수 값 속성을 포함한 모든 자체 속성의 연관 array |
| 화살표 함수 | array (대개 비어 있음) |
function 키워드로 선언된 function 또는 자기 자신을 참조하는 객체 |
변환하지 않음: 복사가 끝나지 않아 PHP 메모리가 부족해집니다(이슈 #21) |
아직 호스트 함수에서 JS 콜백을 받을 방법은 없습니다. 이 경계를 넘어갈 때는 함수가 아닌 데이터를 전달하세요.
숫자와 정밀도
JavaScript 숫자는 IEEE 754 배정밀도입니다. PHP 정수는 플랫폼 고유 형식입니다(모든 최신 빌드에서 64비트). 변환은 ±2^53까지의 정수를 정확하게 보존합니다. 더 큰 PHP 정수는 number이 되며 소수점 이하 16자리 이후의 정밀도를 잃습니다. 그보다 큰 정수도 정확히 계산해야 한다면 JS 내부에서 BigInt를 사용하고 결과를 문자열로 전달하세요.
문자열
PHP 문자열은 바이트 배열이고 JS 문자열은 UTF-16 코드 단위 시퀀스입니다. 엔진은 PHP 바이트를 UTF-8로 해석한 뒤 내부에서 UTF-16으로 다시 인코딩하여 두 형식 사이를 변환합니다. 유효한 UTF-8 입력은 손실 없이 변환됩니다. 유효한 UTF-8이 아닌 바이트는 U+FFFD로 대체되지 않고 그대로 전달됩니다.
String.length, 인덱스 접근(s[0]), 슬라이싱은 모두 ECMAScript 사양에 따라 바이트나 코드 포인트가 아닌 UTF-16 코드 단위를 셉니다. 따라서 '🍕'와 같은 보충 평면 문자가 포함된 문자열은 .length === 2을 갖습니다(U+1F355의 서로게이트 상위 및 하위 쌍).
왕복 변환 안정성
일반 데이터는 왕복 변환 후에도 유지됩니다. 스칼라, 리스트, 연관 배열로 이루어진 PHP 값이 setGlobal()을 통해 들어간 뒤 eval() 또는 call()을 통해 돌아오면 동일한 구조가 됩니다. 단, 정수형 실수는 int로 돌아오며(1.0는 1을 반환) 예외입니다.
위 표에서 null, undefined 또는 []로 매핑되는 나머지 값은 왕복 변환되지 않습니다.