From 3deeb72cc093ad5e35348d93746cfa94867838a5 Mon Sep 17 00:00:00 2001 From: PurHur Date: Sun, 7 Jun 2026 21:03:21 +0000 Subject: [PATCH] php-in-php: migrate JSON POST parser from superglobals_refresh.c to LLVM (#7389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete ~340 lines of hand-written sg_json_* C in superglobals_refresh.c; AOT CGI application/json $_POST refresh now calls __phpc_json_parse_post_body from StringJsonDecodeJit (shared with json_decode LLVM helpers). Verification: vendor/bin/phpunit test/unit/JIT/SuperglobalsJsonPostRuntimeStandaloneTest.php wc -l lib/AOT/runtime/superglobals_refresh.c # 2003 → 1663 Co-authored-by: Cursor --- lib/AOT/runtime/superglobals_refresh.c | 354 +----------------- lib/JIT/Builtin/StringJsonDecode.php | 6 + lib/JIT/Builtin/StringJsonDecodeJit.php | 82 +++- lib/JIT/Context.php | 1 + .../StringJsonDecodeRuntimeStandaloneTest.php | 1 + ...erglobalsJsonPostRuntimeStandaloneTest.php | 46 +++ 6 files changed, 140 insertions(+), 350 deletions(-) create mode 100644 test/unit/JIT/SuperglobalsJsonPostRuntimeStandaloneTest.php diff --git a/lib/AOT/runtime/superglobals_refresh.c b/lib/AOT/runtime/superglobals_refresh.c index bce2c24e1c4..5fb9fa46a3c 100644 --- a/lib/AOT/runtime/superglobals_refresh.c +++ b/lib/AOT/runtime/superglobals_refresh.c @@ -49,8 +49,13 @@ extern void __phpc_parse_str_parse_delimited_pairs( ); extern __hashtable__ *__phpc_parse_str_ensure_child(__hashtable__ *ht, const char *key); +/* JSON POST body parsing: PHP LLVM via StringJsonDecodeJit (#7389). */ +extern void __phpc_json_parse_post_body(__hashtable__ *ht, const char *body); + #define sg_ensure_child __phpc_parse_str_ensure_child +#define SG_MAX_BODY (8 * 1024 * 1024) + static __string__ *cstr_to_string(const char *cstr) { size_t len = strlen(cstr); @@ -71,352 +76,7 @@ static void parse_form_encoded(__hashtable__ *ht, const char *body) __phpc_parse_str_parse_delimited_pairs(ht, body, '&', 0); } -#define SG_JSON_MAX_DEPTH 32 -#define SG_JSON_MAX_BODY (8 * 1024 * 1024) - -typedef struct { - const char *pos; - const char *end; - int depth; -} sg_json_ctx; - -static void sg_json_skip_ws(sg_json_ctx *ctx) -{ - while (ctx->pos < ctx->end) { - char c = *ctx->pos; - - if (' ' != c && '\t' != c && '\n' != c && '\r' != c) { - break; - } - ctx->pos++; - } -} - -static int sg_json_expect(sg_json_ctx *ctx, char ch) -{ - sg_json_skip_ws(ctx); - if (ctx->pos >= ctx->end || *ctx->pos != ch) { - return 0; - } - ctx->pos++; - - return 1; -} - -static int sg_json_parse_string(sg_json_ctx *ctx, char *out, size_t out_len) -{ - size_t o = 0; - - if (!sg_json_expect(ctx, '"')) { - return 0; - } - while (ctx->pos < ctx->end) { - char c = *ctx->pos++; - - if ('"' == c) { - out[o] = '\0'; - - return 1; - } - if ('\\' == c && ctx->pos < ctx->end) { - char esc = *ctx->pos++; - - if ('"' == esc || '\\' == esc || '/' == esc) { - c = esc; - } else if ('b' == esc) { - c = '\b'; - } else if ('f' == esc) { - c = '\f'; - } else if ('n' == esc) { - c = '\n'; - } else if ('r' == esc) { - c = '\r'; - } else if ('t' == esc) { - c = '\t'; - } else if ('u' == esc) { - /* Minimal UTF-8: skip \uXXXX (issue #52 subset). */ - if (ctx->pos + 4 > ctx->end) { - return 0; - } - ctx->pos += 4; - continue; - } else { - return 0; - } - } - if (o + 1 >= out_len) { - return 0; - } - out[o++] = c; - } - - return 0; -} - -static int sg_json_parse_number(sg_json_ctx *ctx, char *out, size_t out_len) -{ - const char *start = ctx->pos; - size_t len; - - if (ctx->pos >= ctx->end) { - return 0; - } - if ('-' == *ctx->pos) { - ctx->pos++; - } - if (ctx->pos >= ctx->end || (*ctx->pos < '0' || *ctx->pos > '9')) { - return 0; - } - while (ctx->pos < ctx->end && *ctx->pos >= '0' && *ctx->pos <= '9') { - ctx->pos++; - } - if (ctx->pos < ctx->end && '.' == *ctx->pos) { - ctx->pos++; - while (ctx->pos < ctx->end && *ctx->pos >= '0' && *ctx->pos <= '9') { - ctx->pos++; - } - } - if (ctx->pos < ctx->end && ('e' == *ctx->pos || 'E' == *ctx->pos)) { - ctx->pos++; - if (ctx->pos < ctx->end && ('+' == *ctx->pos || '-' == *ctx->pos)) { - ctx->pos++; - } - while (ctx->pos < ctx->end && *ctx->pos >= '0' && *ctx->pos <= '9') { - ctx->pos++; - } - } - len = (size_t) (ctx->pos - start); - if (len + 1 > out_len) { - return 0; - } - memcpy(out, start, len); - out[len] = '\0'; - - return 1; -} - -static int sg_json_parse_literal(sg_json_ctx *ctx, const char *lit, char *out, size_t out_len) -{ - size_t len = strlen(lit); - - if (ctx->pos + len > ctx->end || 0 != strncmp(ctx->pos, lit, len)) { - return 0; - } - ctx->pos += len; - strncpy(out, lit, out_len - 1); - out[out_len - 1] = '\0'; - - return 1; -} - -static int sg_json_store_scalar( - __hashtable__ *ht, - const char *key, - int use_index, - size_t index, - const char *value -) -{ - if (use_index) { - __hashtable__setStringAt(ht, index, cstr_to_string(value)); - - return 1; - } - set_string_key(ht, key, value); - - return 1; -} - -static int sg_json_parse_value( - sg_json_ctx *ctx, - __hashtable__ *ht, - const char *key, - int use_index, - size_t index -); - -static int sg_json_parse_array(sg_json_ctx *ctx, __hashtable__ *ht, const char *key); - -static int sg_json_parse_object(sg_json_ctx *ctx, __hashtable__ *ht) -{ - char key_buf[256]; - - if (!sg_json_expect(ctx, '{')) { - return 0; - } - sg_json_skip_ws(ctx); - if (sg_json_expect(ctx, '}')) { - return 1; - } - for (;;) { - if (!sg_json_parse_string(ctx, key_buf, sizeof(key_buf)) || '\0' == key_buf[0]) { - return 0; - } - if (!sg_json_expect(ctx, ':')) { - return 0; - } - if (!sg_json_parse_value(ctx, ht, key_buf, 0, 0)) { - return 0; - } - sg_json_skip_ws(ctx); - if (ctx->pos >= ctx->end) { - return 0; - } - if ('}' == *ctx->pos) { - ctx->pos++; - - return 1; - } - if (',' != *ctx->pos) { - return 0; - } - ctx->pos++; - } -} - -static int sg_json_parse_array(sg_json_ctx *ctx, __hashtable__ *ht, const char *key) -{ - size_t idx = 0; - __hashtable__ *list_ht; - - if (!sg_json_expect(ctx, '[')) { - return 0; - } - sg_json_skip_ws(ctx); - if (sg_json_expect(ctx, ']')) { - list_ht = sg_ensure_child(ht, key); - return 1; - } - list_ht = sg_ensure_child(ht, key); - for (;;) { - if (!sg_json_parse_value(ctx, list_ht, NULL, 1, idx)) { - return 0; - } - idx++; - sg_json_skip_ws(ctx); - if (ctx->pos >= ctx->end) { - return 0; - } - if (']' == *ctx->pos) { - ctx->pos++; - - return 1; - } - if (',' != *ctx->pos) { - return 0; - } - ctx->pos++; - } -} - -static int sg_json_parse_value( - sg_json_ctx *ctx, - __hashtable__ *ht, - const char *key, - int use_index, - size_t index -) -{ - char val_buf[4096]; - - if (ctx->depth > SG_JSON_MAX_DEPTH) { - return 0; - } - sg_json_skip_ws(ctx); - if (ctx->pos >= ctx->end) { - return 0; - } - if ('"' == *ctx->pos) { - if (!sg_json_parse_string(ctx, val_buf, sizeof(val_buf))) { - return 0; - } - - return sg_json_store_scalar(ht, key, use_index, index, val_buf); - } - if ('{' == *ctx->pos) { - __hashtable__ *child; - - if (use_index || NULL == key) { - return 0; - } - ctx->depth++; - child = sg_ensure_child(ht, key); - if (!sg_json_parse_object(ctx, child)) { - ctx->depth--; - - return 0; - } - ctx->depth--; - - return 1; - } - if ('[' == *ctx->pos) { - if (use_index || NULL == key) { - return 0; - } - ctx->depth++; - if (!sg_json_parse_array(ctx, ht, key)) { - ctx->depth--; - - return 0; - } - ctx->depth--; - - return 1; - } - if ('-' == *ctx->pos || (*ctx->pos >= '0' && *ctx->pos <= '9')) { - if (!sg_json_parse_number(ctx, val_buf, sizeof(val_buf))) { - return 0; - } - - return sg_json_store_scalar(ht, key, use_index, index, val_buf); - } - if ('t' == *ctx->pos) { - if (!sg_json_parse_literal(ctx, "true", val_buf, sizeof(val_buf))) { - return 0; - } - - return sg_json_store_scalar(ht, key, use_index, index, "1"); - } - if ('f' == *ctx->pos) { - if (!sg_json_parse_literal(ctx, "false", val_buf, sizeof(val_buf))) { - return 0; - } - - return sg_json_store_scalar(ht, key, use_index, index, ""); - } - if ('n' == *ctx->pos) { - if (!sg_json_parse_literal(ctx, "null", val_buf, sizeof(val_buf))) { - return 0; - } - - return sg_json_store_scalar(ht, key, use_index, index, ""); - } - - return 0; -} - -static void parse_json_post(__hashtable__ *ht, const char *body) -{ - sg_json_ctx ctx; - - if (NULL == body || '\0' == body[0]) { - return; - } - if (strlen(body) > SG_JSON_MAX_BODY) { - return; - } - ctx.pos = body; - ctx.end = body + strlen(body); - ctx.depth = 0; - sg_json_skip_ws(&ctx); - if (ctx.pos >= ctx.end || '{' != *ctx.pos) { - return; - } - (void) sg_json_parse_object(&ctx, ht); -} - -#define SG_MULTIPART_MAX_BODY SG_JSON_MAX_BODY +#define SG_MULTIPART_MAX_BODY SG_MAX_BODY static int sg_extract_boundary(const char *content_type, char *out, size_t out_len) { @@ -741,7 +401,7 @@ static void parse_multipart_post( static void populate_post_body(__hashtable__ *ht, const char *content_type, const char *body) { if (0 == strcmp(content_type, "application/json")) { - parse_json_post(ht, body); + __phpc_json_parse_post_body(ht, body); } else if (0 == strncmp(content_type, "multipart/form-data", 19)) { parse_multipart_post(ht, sg_FILES, content_type, body); } else { diff --git a/lib/JIT/Builtin/StringJsonDecode.php b/lib/JIT/Builtin/StringJsonDecode.php index faf1e2cd32b..504aa7608a1 100644 --- a/lib/JIT/Builtin/StringJsonDecode.php +++ b/lib/JIT/Builtin/StringJsonDecode.php @@ -16,6 +16,12 @@ public static function ensureLinked(Context $context): void self::implement($context); } + /** Standalone AOT: JSON POST helper for superglobals_refresh.c (#7389). */ + public static function ensureStandaloneBodies(Context $context): void + { + StringJsonDecodeJit::ensureStandaloneBodies($context); + } + public static function implement(Context $context): void { $resume = null; diff --git a/lib/JIT/Builtin/StringJsonDecodeJit.php b/lib/JIT/Builtin/StringJsonDecodeJit.php index 46c3ddf60d0..1db9640170f 100644 --- a/lib/JIT/Builtin/StringJsonDecodeJit.php +++ b/lib/JIT/Builtin/StringJsonDecodeJit.php @@ -35,6 +35,12 @@ final class StringJsonDecodeJit /** @var Value|null */ private static $lastErrorGlobal = null; + /** Standalone AOT: JSON POST helper for superglobals_refresh.c (#7389). */ + public static function ensureStandaloneBodies(Context $context): void + { + self::implement($context); + } + public static function implement(Context $context): void { $restore = self::captureInsertBlock($context); @@ -45,9 +51,6 @@ public static function implement(Context $context): void $probe = $context->module->getNamedFunction('__compiler_json_decode'); if (null !== $probe && $probe->countBasicBlocks() > 0) { $context->registerFunction('__compiler_json_decode', $probe); - self::restoreInsertBlock($context, $restore); - - return; } foreach ( @@ -67,6 +70,7 @@ public static function implement(Context $context): void '__phpc_json_parse_array', '__phpc_json_parse_value', '__phpc_json_parse_top', + '__phpc_json_parse_post_body', '__compiler_json_decode', '__compiler_json_validate', '__compiler_json_last_error', @@ -96,6 +100,7 @@ public static function implement(Context $context): void self::implementIfMissing($context, '__phpc_json_parse_array', self::emitParseArray(...)); self::implementIfMissing($context, '__phpc_json_parse_value', self::emitParseValue(...)); self::implementIfMissing($context, '__phpc_json_parse_top', self::emitParseTop(...)); + self::implementIfMissing($context, '__phpc_json_parse_post_body', self::emitParsePostBody(...)); self::implementIfMissing($context, '__compiler_json_decode', self::emitCompilerJsonDecode(...)); self::implementIfMissing($context, '__compiler_json_validate', self::emitCompilerJsonValidate(...)); self::implementIfMissing($context, '__compiler_json_last_error', self::emitCompilerJsonLastError(...)); @@ -204,6 +209,10 @@ private static function declareFunction(Context $context, string $name): LlvmFun $name, $context->context->functionType($i32, false, $i8pp, $i8p, $i32p, $i32, $valuePtr) ), + '__phpc_json_parse_post_body' => $context->module->addFunction( + $name, + $context->context->functionType($void, false, $htPtr, $i8p) + ), '__compiler_json_decode' => $context->module->addFunction( $name, $context->context->functionType($void, false, $strPtr, $valuePtr) @@ -1541,6 +1550,73 @@ private static function emitParseValue(Context $context, LlvmFunction $fn): void $context->builder->returnValue($zeroI32); } + /** CGI application/json $_POST refresh — superglobals_refresh.c (#7389). */ + private static function emitParsePostBody(Context $context, LlvmFunction $fn): void + { + $entry = $fn->appendBasicBlock('entry'); + $context->builder->positionAtEnd($entry); + + $i8 = $context->getTypeFromString('int8'); + $i32 = $context->getTypeFromString('int32'); + $i8p = $context->getTypeFromString('int8*'); + $sizeT = $context->getTypeFromString('size_t'); + $maxLen = $sizeT->constInt(self::MAX_LEN, false); + $zeroI8 = $i8->constInt(0, false); + $maxDepth = $i32->constInt(self::MAX_DEPTH, false); + + $ht = $fn->getParam(0); + $body = $fn->getParam(1); + + $ret = $fn->appendBasicBlock('ret'); + $work = $fn->appendBasicBlock('work'); + $lenCheck = $fn->appendBasicBlock('len_check'); + $parse = $fn->appendBasicBlock('parse'); + $doParse = $fn->appendBasicBlock('do_parse'); + + $isNull = $context->builder->icmp(Builder::INT_EQ, $body, $i8p->constNull()); + $context->builder->branchIf($isNull, $ret, $work); + + $context->builder->positionAtEnd($work); + $first = $context->builder->load($body); + $empty = $context->builder->icmp(Builder::INT_EQ, $first, $zeroI8); + $context->builder->branchIf($empty, $ret, $lenCheck); + + $context->builder->positionAtEnd($lenCheck); + $len = $context->builder->call($context->lookupFunction('strlen'), $body); + $tooLong = $context->builder->icmp(Builder::INT_UGT, $len, $maxLen); + $context->builder->branchIf($tooLong, $ret, $parse); + + $context->builder->positionAtEnd($parse); + $posSlot = BasicBlockHelper::entryAlloca($context, $i8p); + $depthSlot = BasicBlockHelper::entryAlloca($context, $i32); + $end = $context->builder->inBoundsGEP($body, $len); + $context->builder->store($body, $posSlot); + $context->builder->store($i32->constInt(0, false), $depthSlot); + $context->builder->call($context->lookupFunction('__phpc_json_skip_ws'), $posSlot, $end); + $pos = $context->builder->load($posSlot); + $atEnd = $context->builder->icmp(Builder::INT_UGE, $pos, $end); + $notObj = $context->builder->icmp( + Builder::INT_NE, + $context->builder->load($pos), + $i8->constInt(ord('{'), false) + ); + $context->builder->branchIf($context->builder->or($atEnd, $notObj), $ret, $doParse); + + $context->builder->positionAtEnd($doParse); + $context->builder->call( + $context->lookupFunction('__phpc_json_parse_object'), + $posSlot, + $end, + $depthSlot, + $maxDepth, + $ht + ); + $context->builder->branch($ret); + + $context->builder->positionAtEnd($ret); + $context->builder->returnVoid(); + } + private static function emitParseTop(Context $context, LlvmFunction $fn): void { $entry = $fn->appendBasicBlock('entry'); diff --git a/lib/JIT/Context.php b/lib/JIT/Context.php index fcd046da007..afa65ee005e 100755 --- a/lib/JIT/Context.php +++ b/lib/JIT/Context.php @@ -646,6 +646,7 @@ private function defineBuiltins(int $loadType): void { Builtin\CliArgvRuntime::ensureStandaloneBodies($this); Builtin\Sscanf::ensureStandaloneBodies($this); Builtin\StringParseStrJit::ensureStandaloneBodies($this); + Builtin\StringJsonDecode::ensureStandaloneBodies($this); Builtin\StringGetenv::ensureStandaloneBodies($this); Builtin\GcCollectCyclesRuntime::ensureStandaloneBodies($this); } diff --git a/test/unit/JIT/StringJsonDecodeRuntimeStandaloneTest.php b/test/unit/JIT/StringJsonDecodeRuntimeStandaloneTest.php index a6aa82601d5..fc2243a0888 100644 --- a/test/unit/JIT/StringJsonDecodeRuntimeStandaloneTest.php +++ b/test/unit/JIT/StringJsonDecodeRuntimeStandaloneTest.php @@ -19,6 +19,7 @@ public function testRuntimeShrinkRemovesJsonDecodeC(): void $jit = (string) file_get_contents(__DIR__.'/../../../lib/JIT/Builtin/StringJsonDecodeJit.php'); $this->assertStringContainsString('__compiler_json_decode', $jit); $this->assertStringContainsString('__compiler_json_validate', $jit); + $this->assertStringContainsString('__phpc_json_parse_post_body', $jit); $this->assertStringContainsString('__compiler_json_last_error', $jit); $this->assertStringContainsString('__compiler_json_last_error_msg', $jit); $linker = (string) file_get_contents(__DIR__.'/../../../lib/AOT/Linker.php'); diff --git a/test/unit/JIT/SuperglobalsJsonPostRuntimeStandaloneTest.php b/test/unit/JIT/SuperglobalsJsonPostRuntimeStandaloneTest.php new file mode 100644 index 00000000000..4d21393fe7f --- /dev/null +++ b/test/unit/JIT/SuperglobalsJsonPostRuntimeStandaloneTest.php @@ -0,0 +1,46 @@ +lookupFunction($name); + $this->assertNotNull($fn, $name.' must be linked for standalone AOT'); + $this->assertGreaterThan(0, $fn->countBasicBlocks(), $name.' must have LLVM body'); + } + } + + public function testSuperglobalsRefreshCDoesNotDefineJsonParser(): void + { + $source = (string) file_get_contents(__DIR__.'/../../../lib/AOT/runtime/superglobals_refresh.c'); + $this->assertStringNotContainsString('sg_json_parse_object', $source); + $this->assertStringNotContainsString('sg_json_ctx', $source); + $this->assertStringNotContainsString('parse_json_post', $source); + $this->assertStringContainsString('__phpc_json_parse_post_body', $source); + } +}