본문으로 건너뛰기
5분 AI.
TRENDSHIFT DAILY #23 딥다이브 · 2026-10-23 분석

php-src 딥다이브
— 한 줄의 PHP가 실행되기까지

언어 런타임

빠른 판단

무엇
PHP를 내부 명령으로 컴파일한 뒤 가상 머신으로 실행하는 공식 인터프리터의 C 중심 소스
이럴 때
PHP 런타임을 빌드하거나 언어의 파싱·바이트코드·메모리·확장 모듈 구조를 소스 수준에서 공부할 때
입력
PHP 소스와 실행·요청 환경
결과
컴파일한 내부 명령의 실행 결과
echo "안녕";이라는 문장을 컴퓨터는 어떻게 이해할까요? PHP 인터프리터의 C 소스를 따라가며 구문 분석, 가상 머신, 메모리 관리, 웹 요청의 수명을 배웁니다. 분석 대상은 php/php-src · 커밋 02207318입니다. ⭐41,332 · 포크 8,330 · 주 언어 C · 2026-10-23 GitHub 기준입니다. 현재 소스의 버전은 8.7.0-dev로, 최신 릴리스 php-8.5.12(2026-10-21)와 구분합니다.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

PHP로 만든 서비스 아래에서 돌아가는 실행기의 설계도

php-src는 PHP 소스 코드를 읽고 실행하는 인터프리터의 공식 구현입니다. 웹사이트의 게시판이나 로그인 화면을 만드는 애플리케이션이 아니라, 그런 애플리케이션을 실행하는 프로그램 자체를 C 중심으로 구현한 저장소입니다.

PHP는 실행할 코드를 먼저 내부 명령으로 컴파일한 뒤 가상 머신으로 실행합니다. 따라서 “인터프리터니까 컴파일 단계가 없다”고 생각하면 이 저장소의 절반을 놓칩니다. 어휘 분석기, 파서, 컴파일러, 실행기, 메모리 관리기가 모두 등장합니다.

용어
인터프리터와 가상 머신
인터프리터는 프로그램을 읽고 실행해 주는 소프트웨어입니다. 여기서 가상 머신(VM)은 가상 컴퓨터 한 대를 켜는 장치가 아니라, ZEND_ECHO처럼 PHP 엔진이 이해하는 명령을 처리하는 실행기입니다.
전체를 이해하는 비유

PHP 코드는 주문서, Zend Engine은 주방, SAPI는 주문을 받는 창구입니다

터미널에서 주문하든 웹서버를 통해 주문하든 주방은 같은 조리 규칙을 사용합니다. 주문서를 단어로 나누고, 뜻을 해석하고, 작업 지시서로 바꾼 뒤 실행합니다.

이미 해석한 작업 지시서를 보관하는 것이 OPcache입니다. 완성된 음식에 해당하는 HTTP 응답을 그대로 저장하는 캐시와는 역할이 다릅니다.

1-1. 같은 언어가 여러 환경에서 실행되는 이유

사용 장면저장소의 담당 부품초보자가 배울 질문
터미널 실행sapi/cli/명령행 인자와 표준 출력이 엔진에 어떻게 연결될까?
웹 요청 처리sapi/fpm/프로세스는 살아 있는데 요청 변수는 왜 정리될까?
언어 실행Zend/문법, 자료형, 함수 호출을 C로 어떻게 표현할까?
내장 기능ext/JSON·정규식·데이터베이스 기능은 어떻게 등록될까?

예를 들어 CLI의 일반 파일 실행 경로는 sapi/cli/php_cli.c에서 php_request_startup()을 호출하고, php_execute_script()로 파일을 실행합니다. 웹 요청이 아니어도 엔진 내부에서는 “요청을 시작하고 끝내는 단위”를 사용합니다.

이 글의 학습 목표
언어를 사용하는 시선에서, 언어를 구현하는 시선으로

C 문법을 모두 외우고 시작할 필요는 없습니다. 함수 하나의 입력과 출력, 메모리를 누가 소유하는지, 다음 단계에 무엇을 넘기는지부터 확인하면 됩니다. 모든 소스 경로는 php/php-src 저장소 루트 기준입니다.

2왜 주목받는가

트렌딩 사실과, 소스에서 읽어 낼 수 있는 가치를 구분하기

2-1. 최근 릴리스와 개발 브랜치를 동시에 볼 수 있다

선정 근거는 TrendShift Daily #23, 분석일 2026-10-23입니다. GitHub 메타에는 이틀 전인 2026-10-21의 php-8.5.12 릴리스와 2026-10-23의 최근 push가 기록되어 있습니다. 다만 이 정보만으로 어떤 기능이나 사건이 트렌딩을 일으켰다고 확정할 수는 없습니다.

학습 자료로서의 해석: 안정 릴리스가 배포되는 동안에도 다음 개발 버전의 코드가 움직이고, 여러 브랜치를 대상으로 CI를 운영하는 모습을 함께 관찰할 수 있습니다. “유명한 언어”라는 사실보다 성숙한 실행기를 계속 바꾸면서 호환성을 지키는 방법이 좋은 공부거리입니다.

버전 구분
커밋 메시지의 PHP-8.6을 실행기 버전으로 읽으면 안 됩니다

HEAD 메시지는 Merge branch 'PHP-8.6'이지만, configure.ac:26의 AC_INIT과 main/php_version.h는 모두 8.7.0-dev를 가리킵니다. 병합한 브랜치 이름과 현재 트리의 버전은 서로 다른 정보입니다. 이 글의 내부 구현을 8.5.12의 동작이라고 그대로 옮겨 적으면 안 됩니다.

2-2. 비슷한 실행기를 공부한다면 무엇이 다를까?

아래는 학습 관점의 구조 비교입니다. 처리량이나 메모리 사용량을 측정한 순위표는 아닙니다. 선택 기준을 “내가 설명하고 싶은 실행 모델이 무엇인가”에 두면 각 구현의 장점이 보입니다.

비교 대상살펴보기 좋은 관점php-src에서 특히 잘 드러나는 점
CPython동적 언어의 객체·바이트코드·참조 수 관리SAPI와 요청 수명 주기를 통해 웹 실행 환경과 언어 엔진의 경계를 읽기 좋습니다.
Node.js / V8이벤트 루프, 비동기 I/O와 JavaScript 실행CLI와 FPM을 나란히 읽으며 프로세스 수명과 요청 수명을 구분하기 좋습니다.
직접 만든 인터프리터짧은 코드로 언어의 기본 원리 파악오류·확장·운영체제·회귀 테스트까지 실제 구현이 감당하는 복잡성을 볼 수 있습니다.

php-src의 장점은 언어 처리 파이프라인과 웹 실행 환경이 한 저장소에서 연결된다는 점입니다. 반대로 학습량도 큽니다. 처음부터 모든 확장과 JIT를 이해하려 하기보다 출력문 하나를 끝까지 따라가는 편이 시작하기 쉽습니다.

2-3. 23,655개의 실행 가능한 사례집

커밋 02207318의 .phpt 파일은 23,655개입니다. 그중 ext/에 16,675개, Zend/에 5,707개, tests/에 904개, sapi/에 369개가 있습니다. 이 숫자는 테스트 파일 수이며, 서로 독립적인 버그 수나 테스트 통과 건수를 뜻하지 않습니다.

큰 저장소에서는 설명 문서와 함께 작은 회귀 테스트를 읽어야 실제 동작의 경계가 보입니다. 문자열 길이 테스트만 보아도 빈 문자열, NUL 바이트, 공백처럼 쉽게 놓치는 입력을 만날 수 있습니다.

비유

문법 매뉴얼이 교통 법규라면 회귀 테스트는 실제 사고를 예방하는 사례집입니다. 정상 도로만 달려서는 보이지 않는 경계 상황을 짧은 입력과 기대 출력으로 남깁니다.

3기술 스택 전체 지도

웹 앱의 스택 표를, 언어 실행기의 실제 부품에 맞춰 읽기

3-1. 백엔드: C11로 만든 실행기와 C/C++ 확장

중심 구현 언어는 C입니다. configure.ac:124–126은 C11을 받아들이지 못하는 컴파일러에서 설정을 중단합니다. ext/intl/config.m4에는 C++ 소스 목록과 PHP_REQUIRE_CXX()도 등장합니다. 따라서 “전부 순수 C”보다는 C 중심이며 일부 확장에 C++를 사용하는 프로젝트라고 읽는 편이 정확합니다.

계층기술 / 실제 파일역할
언어 엔진Zend/ · C11컴파일, opcode 실행, 자료형, 함수·클래스, 메모리 관리
공통 실행 환경main/main.c, main/SAPI.h시작·종료, 입력·출력, 설정, SAPI 연결
확장 기능ext/json/, ext/pcre/, ext/intl/JSON, 정규식, 국제화 등 기능별 모듈
실행 창구sapi/cli/, sapi/fpm/터미널과 FastCGI 요청을 공통 엔진에 연결
성능 계층ext/opcache/, ext/opcache/jit/ir/컴파일 결과 캐시, 최적화, 선택적 JIT 코드 생성

3-2. 프론트엔드: 서비스 화면과 컴파일러 앞단은 다른 뜻이다

이 저장소에는 React·Vue 같은 제품 화면용 프론트엔드 앱이 없습니다. 추적 파일 목록에서 package.json, Cargo.toml, go.mod, pyproject.toml, composer.json은 발견되지 않았습니다. 그렇다고 PHP 생태계 전체가 Composer를 쓰지 않는다는 뜻은 아닙니다. 이 저장소의 빌드 방식에 대한 관찰입니다.

컴파일러 분야의 프론트엔드는 별개입니다. Zend/zend_language_scanner.l이 글자를 토큰으로 나누고, Zend/zend_language_parser.y가 토큰을 AST로 묶습니다. 개발 문서 사이트는 docs/requirements.txt에 적힌 Sphinx와 테마로 생성합니다. 이 파일의 네 의존성은 버전이 고정되어 있지 않습니다.

용어
토큰 · AST · opcode
토큰은 echo나 변수처럼 문장을 이루는 조각입니다. AST는 “무엇을 무엇에 더한다”처럼 의미 관계를 담은 나무이고, opcode는 그 의미를 실행기가 처리할 명령으로 바꾼 결과입니다.

3-3. 의존성 버전: 최소 요구와 번들 버전을 구분하자

항목소스에서 읽은 값확인한 파일 / 의미
PHP 개발 버전8.7.0-dev / ID 80700configure.ac, main/php_version.h
Autoconf최소 2.71configure.ac의 AC_PREREQ
Bison / re2c최소 3.0.0 / 1.0.3configure.ac:160–161의 생성 도구 검사
기존 PHP최소 8.1configure.ac:162–163 · build/gen_stub.php용 도구 탐색
번들 PCRE210.44, 날짜 2024-06-07ext/pcre/pcre2lib/pcre2.h:44–47
외부 PCRE2최소 10.30ext/pcre/config0.m4 · 외부 라이브러리 선택 시
libxml2 / SQLite최소 2.9.4 / 3.7.17build/php.m4의 공통 설정 검사
OpenSSL / ICU최소 1.1.1 / 57.1build/php.m4 · 해당 확장 선택 시
intl의 C++ 표준ICU 74 이상이면 C++17, 그 아래는 C++11ext/intl/config.m4:93–102의 조건부 검사
문서 생성Sphinx, sphinx-design, sphinxawesome-theme, rstfmtdocs/requirements.txt · 버전 고정 없음

이 값들은 빌드 검사에 명시된 요구 버전과 번들 소스의 버전입니다. 특히 >= 조건은 “이 버전 하나로 고정”을 뜻하지 않습니다. README는 최소 빌드에 Autoconf·Bison·re2c, 기본 빌드에 libxml2·libsqlite3가 더 필요하다고 설명합니다. 실제 활성화할 확장에 따라 추가 라이브러리는 달라집니다.

3-4. 인프라·빌드: 운영체제 차이를 설정 단계에서 처리한다

Git 소스 | v buildconf -- Autoconf / autoheader | v configure -- C11, 헤더, 함수, 선택한 라이브러리 검사 | v Makefile -- C/C++ 컴파일과 링크 | +-- sapi/cli/php +-- sapi/fpm/php-fpm (FPM 선택 시)

위는 Git 체크아웃에서 시작하는 Unix 계열의 대표 빌드 흐름입니다. Windows는 buildconf.bat, win32/build/config.w32 등 별도 경로를 사용합니다. buildconf의 설명대로 공식 릴리스 압축 파일에는 생성된 configure가 포함되어 있어 Git 체크아웃과 준비 단계가 같지 않습니다.

.github/workflows/test.yml은 브랜치별 행렬을 생성하고 .github/workflows/test-suite.yml을 재사용합니다. 확인한 설정에는 actions/checkout@v6, actions/cache@v5, alpine:3.22, clang-20, mysql:8.4가 나옵니다. ASan·UBSan, debug/release, ZTS/NTS 조합을 다루며 이 값들은 CI 설정값입니다.

ext/opcache/jit/Dockerfile.arm64.example의 기본 이미지는 ubuntu:20.04이고, benchmark/docker-compose.yml에는 mysql:8.0이 있습니다. 각각 JIT 실습 예시와 벤치마크용 보조 서비스입니다. 프로젝트 전체를 배포하는 단일 Docker 환경으로 해석하면 안 됩니다.

실행기의 의존성은 패키지 목록뿐 아니라 빌드 검사와 확장 선택 조건까지 읽어야 파악됩니다.

4아키텍처 심화 분석

네 가지 설계: SAPI 어댑터, 컴파일 파이프라인, 값 공유, 컴파일 훅

4-1. 시스템 구조도: 입력에서 출력까지

[터미널] [브라우저] | | v v CLI SAPI 외부 웹서버 sapi/cli/ | | | FastCGI | v | FPM SAPI / 작업 프로세스 | sapi/fpm/ +----------------+---------------+ | v main/ : 요청 준비, 설정, 출력 | v Zend/ : 언어 실행 파이프라인 PHP 파일 -> zend_compile_file (OPcache 사용 시 캐시 검사) | | 캐시 재사용 가능 새 컴파일 필요 | v | 토큰 -> AST -> zend_op_array | | +------------+-------------+ v VM 실행 -> ext/ 기능 호출 JSON / PCRE / SQLite ... 공통 기반: zval / zend_string / HashTable / 참조 수 / 메모리 할당 출력 경로: VM -> Zend 쓰기 콜백 -> main/output.c -> SAPI

위 그림은 대표 경로를 줄여 그린 것입니다. 그림의 외부 웹서버는 php-src 밖의 구성 요소이고, FPM은 웹서버와 FastCGI로 통신하는 PHP 실행 환경입니다. OPcache를 사용할 수 있는 상황에서는 저장된 컴파일 결과를 재사용합니다. 모든 요청이 항상 어휘 분석부터 다시 시작하는 것은 아닙니다.

4-2. 설계 ① SAPI: 환경 차이를 함수 포인터 표로 감싼다

main/SAPI.h:230의 sapi_module_struct는 출력·헤더·입력·로그 같은 동작을 함수 포인터로 받습니다. C에 클래스가 없어도 “공통 인터페이스 + 환경별 구현”을 만들 수 있습니다. 어댑터나 전략 패턴으로 이해할 수 있는 구조입니다.

// main/SAPI.h — 구조체 필드 일부 발췌
size_t (*ub_write)(const char *str, size_t str_length);
void (*flush)(void *server_context);
size_t (*read_post)(char *buffer, size_t count_bytes);
char *(*read_cookies)(void);
void (*register_server_variables)(zval *track_vars_array);

sapi/cli/php_cli.c:407의 cli_sapi_module은 여기에 sapi_cli_ub_write, sapi_cli_flush 등을 꽂습니다. 엔진은 “지금 출력이 터미널로 가는가, FastCGI로 가는가”의 세부 구현을 몰라도 됩니다.

main/main.c의 모듈 시작 코드가 Zend 쓰기 콜백에 php_output_write를 연결하고, main/output.c가 버퍼를 처리한 뒤 sapi_module.ub_write를 호출합니다. echo를 무조건 C의 printf() 한 번으로 설명할 수 없는 이유입니다.

같은 어댑터 위에서 지키는 수명 주기

범위대표 함수 / 소스무엇을 준비하고 정리하나?
모듈 시작php_module_startup()
main/main.c
엔진, 확장, 설정 등 실행 환경을 준비합니다.
요청 시작php_request_startup()
main/main.c:1853
출력, 전역 변수, SAPI, 확장의 요청 초기화를 진행합니다.
스크립트 실행php_execute_script_ex()
main/main.c:2563
실행 파일과 앞뒤 자동 포함 파일 등을 처리합니다.
요청 종료php_request_shutdown()
main/main.c:1943
종료 함수, 소멸자, 출력 버퍼, 확장과 요청 데이터를 순서대로 정리합니다.

sapi/fpm/fpm/fpm_main.c:1804는 fcgi_accept_request() 반복문 안에서 요청 시작·실행·종료를 호출합니다. 작업 프로세스는 여러 요청을 받을 수 있지만 각 요청의 상태는 따로 정리합니다. 이를 “매 요청마다 운영체제 프로세스를 새로 만든다”와 혼동하지 마세요.

비유

식당은 영업 중 계속 열려 있지만 손님이 바뀔 때마다 식탁을 정리합니다. 프로세스의 수명은 식당의 영업 시간, 요청의 수명은 한 손님의 식사 시간에 가깝습니다. 확장 개발자는 어떤 상태를 식탁 위에 두고 어떤 상태를 주방에 둘지 구분해야 합니다.

4-3. 설계 ② 파이프라인: 문법과 실행을 중간 표현으로 분리한다

추적할 예제는 echo $message;입니다. 파서는 “문자열을 화면에 쓰는 C 코드”를 바로 실행하지 않습니다. 먼저 ZEND_AST_ECHO 노드를 만듭니다.

// Zend/zend_language_parser.y:1205 — 문법 규칙 발췌
echo_expr:
    expr { $$ = zend_ast_create(ZEND_AST_ECHO, $1); }
;

Zend/zend_language_scanner.l의 compile_file()은 파일을 스캐너에 연결한 뒤 zend_compile()로 들어갑니다. 그 안에서 zendparse()가 성공하면 zend_compile_ast_internal()이 zend_compile_top_stmt()를 호출해 AST를 명령 배열로 바꿉니다.

// Zend/zend_compile.c:6400 — 출력문 컴파일 함수
static void zend_compile_echo(const zend_ast *ast)
{
    zend_op *opline;
    zend_ast *expr_ast = ast->child[0];

    znode expr_node;
    zend_compile_expr(&expr_node, expr_ast);

    opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);
    opline->extended_value = 0;
}

앞의 표현식을 컴파일하고 ZEND_ECHO 명령을 추가합니다. 결과는 zend_op_array에 담깁니다. Zend/zend.c:1977의 zend_execute_script()를 보면 zend_compile_file()로 그 배열을 얻어 zend_execute()에 넘깁니다.

// Zend/zend_vm_def.h:1696 — ZEND_ECHO의 문자열 처리 분기 발췌
if (Z_TYPE_P(z) == IS_STRING) {
    zend_string *str = Z_STR_P(z);

    if (ZSTR_LEN(str) != 0) {
        zend_write(ZSTR_VAL(str), ZSTR_LEN(str));
    }
}

전체 핸들러에는 문자열이 아닌 값의 변환, 피연산자 정리, 예외 확인도 있습니다. 여기서는 파서의 의미 노드 → 컴파일러의 opcode → VM의 동작이 연결된다는 점을 먼저 봅니다. 중간 표현을 두면 문법을 해석하는 규칙과 실제로 실행하는 규칙을 따로 발전시킬 수 있습니다.

용어
코드 생성과 특수화
Zend/zend_vm_def.h는 VM 동작을 적는 원본이고, Zend/zend_vm_gen.php가 이를 여러 피연산자 조합에 맞는 코드로 펼칩니다. 생성된 Zend/zend_vm_execute.h는 123,832줄입니다. 이 파일부터 통독하기보다 원본 정의와 Zend/Makefile.frag의 생성 규칙을 먼저 읽으세요.

4-4. 설계 ③ zval과 쓰기 시 복사: 값의 의미와 저장 비용을 함께 지킨다

Zend/zend_types.h:341의 zval은 zend_value라는 값 저장 공간과 타입 정보를 함께 가집니다. 정수·실수는 직접 담을 수 있고, 문자열·배열·객체는 다른 구조체를 가리킵니다. “PHP 변수는 타입이 바뀔 수 있는데 C 코드는 어떻게 처리하지?”에 대한 출발점입니다.

$a의 zval ----+ +----> 공유 배열 저장소 $b의 zval ----+ $b의 원소를 바꾸려 할 때 공유 중이라면: $a의 zval ---------> 기존 배열 $b의 zval ---------> 복사한 배열에 변경 반영

이 방식이 쓰기 시 복사(copy-on-write, COW)입니다. 대입하자마자 큰 배열을 모두 복사하지 않고, 공유 중인 값을 바꾸어야 할 때 분리합니다. 핵심 조건을 SEPARATE_ARRAY에서 확인할 수 있습니다.

// Zend/zend_types.h:1560 — 줄 맞춤만 정리한 매크로 발췌
#define SEPARATE_ARRAY(zv) do { \
    zval *__zv = (zv); \
    zend_array *_arr = Z_ARR_P(__zv); \
    if (UNEXPECTED(GC_REFCOUNT(_arr) > 1)) { \
        ZVAL_ARR(__zv, zend_array_dup(_arr)); \
        GC_TRY_DELREF(_arr); \
    } \
} while (0)

참조 수가 1보다 크면 배열을 복제하고 기존 배열의 참조 수를 줄입니다. 실제 대입·변경 경로에는 추가 조건이 있으므로 이 매크로만으로 모든 PHP 값의 동작을 설명할 수는 없습니다. 객체 대입, & 참조, 배열 안의 객체는 별도로 구분해야 합니다.

흔한 오해
참조 수만으로 모든 메모리가 회수되는 것은 아닙니다

서로를 가리키는 객체나 배열은 외부에서 접근할 수 없어져도 참조 수가 남을 수 있습니다. Zend/zend_gc.c의 zend_gc_collect_cycles()는 이런 순환 관계를 다룹니다. 또 모든 값이 별도의 참조 수를 갖는 것도 아닙니다. 정수·실수 같은 단순 값은 구분해 읽어야 합니다.

4-5. 설계 ④ 컴파일 훅: 기존 동작을 감싸서 캐시한다

Zend/zend_compile.c:90에서 zend_compile_file은 함수 포인터입니다. 일반적인 초기화에서는 compile_file을 가리키지만, OPcache 시작 코드가 원래 함수를 보관하고 새 함수로 바꿉니다. 기존 구현을 보존하며 기능을 덧붙이는 데코레이터와 비슷한 방식입니다.

// ext/opcache/ZendAccelerator.c:3658–3659
accelerator_orig_compile_file = zend_compile_file;
zend_compile_file = persistent_compile_file;

persistent_compile_file()은 캐시 상태와 파일 정보를 확인합니다. 캐시를 사용할 수 없으면 원래 컴파일러에 위임하고, 재사용 가능한 스크립트를 찾으면 그 컴파일 결과를 이용합니다. 기본 경로의 일부는 공유 메모리를 사용하며 파일 캐시 경로도 따로 있습니다.

설정코드의 기본값읽을 때 주의할 점
opcache.enable1OPcache 초기화와 실행 환경 등 다른 조건도 충족해야 합니다.
opcache.enable_cli0CLI와 웹의 관찰 결과가 다를 수 있습니다.
opcache.memory_consumption128 MiBOPcache 예산이지 PHP 프로세스 전체 메모리가 아닙니다.
opcache.jitdisable컴파일 시 JIT 지원과 실행 시 JIT 활성화는 다릅니다.
opcache.jit_buffer_size64Mext/opcache/jit/zend_jit.h의 기본 매크로. 값만으로 JIT 작동을 판정하면 안 됩니다.

위 설정의 기본값은 ext/opcache/zend_accelerator_module.c와 ext/opcache/jit/zend_jit.h에 정의되어 있습니다. OPcache는 컴파일 결과를 재사용하고, JIT는 실행 중 일부 코드를 기계어로 만드는 기능입니다. OPcache가 켜졌다는 사실만으로 JIT도 켜졌다고 결론 낼 수 없습니다.

확장 모듈도 같은 수명 주기에 참여한다

ext/json/json.c:74의 json_module_entry는 함수 목록 ext_functions, 모듈 초기화 PHP_MINIT(json), 요청 초기화 PHP_RINIT(json) 등을 등록합니다. PHP_FUNCTION(json_encode)는 인자를 검사하고 내부 인코더로 연결합니다.

ext/json/config.m4는 PHP_NEW_EXTENSION에 ext/json/json_encoder.c, 생성 파일인 ext/json/json_parser.tab.c와 ext/json/json_scanner.c, ext/json/json.c를 묶습니다. 생성 규칙은 ext/json/Makefile.frag에 있습니다. 기능 등록과 빌드 등록을 함께 봐야 “C 함수가 PHP 함수가 되는 과정”을 이해할 수 있습니다.

5디렉토리 구조 해부

전체를 외우기보다, 질문에 맞춰 들어갈 입구를 고르기
php-src/ (저장소 루트) +-- Zend/ 언어 엔진 | +-- zend_language_scanner.l 어휘 분석 규칙과 컴파일 진입 | +-- zend_language_parser.y 문법 규칙과 AST 생성 | +-- zend_compile.c AST를 opcode로 변환 | +-- zend_vm_def.h VM 명령의 원본 정의 | +-- zend_vm_gen.php VM 코드 생성기 | +-- zend_vm_execute.h 생성된 실행 코드 | +-- zend_types.h zval 등 핵심 자료형 | +-- zend_hash.c HashTable 구현 | +-- zend_alloc.c 메모리 할당기 | +-- zend_gc.c 순환 참조 수집 | +-- Optimizer/ opcode 최적화 | +-- tests/ 언어 엔진 테스트 +-- main/ 엔진과 실행 환경을 연결 | +-- main.c 시작, 실행, 종료 | +-- SAPI.h SAPI 인터페이스 | +-- output.c 출력과 버퍼 +-- sapi/ | +-- cli/ 명령행 실행과 내장 개발 서버 | +-- fpm/ FastCGI 프로세스 관리 | +-- cgi/ CGI / FastCGI 실행 환경 | +-- apache2handler/ Apache 모듈 연결 +-- ext/ 내장·선택 확장과 각 확장의 테스트 | +-- standard/ 표준 기능 | +-- json/ JSON 변환과 검증 | +-- pcre/ 정규식, 번들 PCRE2 | +-- opcache/ 컴파일 결과 캐시와 JIT +-- TSRM/ 스레드 안전 실행의 자원 관리 +-- build/ 빌드 매크로와 생성 도구 +-- win32/ Windows 호환·빌드 코드 +-- tests/ 공통 동작 테스트 +-- .github/ CI 워크플로와 보조 작업 +-- benchmark/ 성능 비교용 도구와 보조 환경 +-- docs/ 내부 문서와 문서 사이트 설정 +-- docs-old/ 이전 내부 문서 +-- scripts/ phpize, php-config 등의 템플릿 +-- configure.ac 빌드 설정의 원본 +-- run-tests.php PHPT 테스트 실행기 +-- php.ini-development 개발 설정 예시 +-- php.ini-production 운영 설정 예시

5-1. 궁금한 내용으로 파일을 찾는 법

질문먼저 읽을 파일다음에 연결할 것
문법이 왜 이렇게 해석되지?Zend/zend_language_parser.y해당 AST 종류 → Zend/zend_compile.c
연산이 실제로 무엇을 하지?Zend/zend_vm_def.h자료형별 분기 → 연산 보조 함수
내장 함수의 경계 입력은?ext/standard/tests/strings/ 등 확장별 테스트테스트명 → C 함수 → 인자 정의
설정값은 어디서 정하지?main/main.c, 확장 모듈의 C 파일INI 등록부 → 변경 콜백 → 실제 사용처

파일명이 *.stub.php라면 PHP 쪽 함수·메서드 선언과 타입 정보를 먼저 볼 수 있습니다. build/gen_stub.php와 build/Makefile.global은 이를 *_arginfo.h 같은 C용 정보로 연결합니다. 생성된 결과와 사람이 수정하는 원본을 구분하는 습관이 중요합니다.

탐색 요령
읽는 파일 수에 목표를 두지 말고, 연결을 하나 완성하세요

echo라면 파서·컴파일러·VM·출력 계층 네 곳을 연결하면 됩니다. 12만 줄짜리 생성 헤더를 처음부터 읽는 것보다, 짧은 정의 파일에서 관심 명령의 입력과 출력을 찾는 편이 효율적입니다.

6학습 포인트 (기술별)

파일을 읽고 끝내지 않고, 설명 가능한 원리로 바꾸기

6-1. C: 포인터와 자료형 태그 읽기

zend_value는 여러 타입을 같은 저장 공간에 담는 union입니다. zval의 타입 정보가 현재 어느 값을 읽어야 하는지 알려 줍니다. C는 그 조합이 맞는지 항상 자동 확인해 주지 않으므로, Z_TYPE_P(), Z_STR_P() 같은 매크로가 중요한 경계가 됩니다.

용어
포인터와 함수 포인터
포인터는 데이터의 위치를 담는 값입니다. ast->child[0]은 포인터가 가리키는 구조체의 필드를 읽는 표기이고, &expr_node는 변수의 주소를 전달합니다. 함수 포인터는 “호출할 함수의 위치”를 담아 SAPI처럼 동작을 바꾸는 데 쓰입니다.

실습 아이디어: zend_compile_echo()의 각 줄 옆에 “AST를 읽음 / 임시 결과를 만듦 / 명령을 추가함”을 적어 보세요. C의 모든 매크로를 펼치기 전에 함수의 책임부터 설명하는 연습입니다.

6-2. 컴파일러: 한 문법을 세 가지 표현으로 그리기

echo $message;를 토큰 목록, AST, opcode 단계로 나누어 적습니다. 파서가 만드는 ZEND_AST_ECHO와 VM이 처리하는 ZEND_ECHO는 이름이 비슷하지만 서로 다른 계층입니다. 이 둘을 연결하는 곳이 컴파일러입니다.

실습 아이디어: echo 1 + 2;의 의미 구조를 먼저 그린 뒤 Zend/zend_compile.c에서 상수 표현식 처리 경로를 찾아보세요. 문법에 덧셈이 있다고 실행 시 반드시 덧셈 opcode가 남는 것은 아닙니다.

6-3. 문자열: 바이트 길이와 사람 눈의 글자 수

Zend/zend_builtin_functions.c:376의 ZEND_FUNCTION(strlen)은 ZSTR_LEN(s)를 반환합니다. 문자열을 끝까지 훑는 대신 zend_string에 저장된 길이를 읽습니다. 이 길이는 바이트 수이므로 UTF-8 한글의 글자 수와 같지 않을 수 있습니다.

// UTF-8로 저장한 PHP 실습 파일의 예시
<?php
var_dump(strlen("가"));     // 예상: int(3)
var_dump(strlen("A\0B"));   // 예상: int(3), NUL도 문자열 내부에 포함

더 흥미로운 점은 C 구현 바로 위 주석이 이 함수가 보통 우회된다고 알린다는 것입니다. Zend/zend_compile.c:4400의 zend_compile_func_strlen()은 상수 문자열이면 길이를 상수로 계산하고, 그 밖의 처리 가능한 경우에는 ZEND_STRLEN 명령을 만듭니다. 인자 수 등 조건이 맞지 않으면 이 특별 처리에서 빠집니다.

함수 이름을 찾았다고 실행 경로를 다 찾은 것은 아닙니다. 컴파일러의 특별 처리도 확인해야 합니다.

실습 아이디어: 상수 문자열과 함수 인자로 전달된 문자열의 경로를 비교하고, “동작의 의미”와 “최적화 후 남는 명령”을 별도로 적어 보세요.

내부 문서와 코드의 차이
문자열 해제 설명에 할당 함수 이름이 적혀 있습니다

docs/source/core/data-structures/zend_string.rst:56은 zend_string_release()가 malloc 또는 emalloc을 호출한다고 설명합니다. 그러나 Zend/zend_string.h:480의 실제 코드는 interned 문자열을 제외하고 참조 수를 줄인 뒤, 0이 되면 pefree()로 해제합니다. Zend/zend_alloc.h:206에서 이 매크로는 free()와 efree()를 선택합니다. 해당 문서의 함수명 표기는 코드와 어긋납니다. 할당과 해제는 반대 동작이므로 구현을 기준으로 읽어야 합니다.

6-4. 테스트: 입력과 기대 출력을 한 파일에 둔다

PHPT의 --TEST--는 설명, --FILE--은 실행할 PHP 코드, --EXPECT--는 정확히 기대하는 출력입니다. --EXPECTF--는 일부 가변 출력을 형식으로 비교하고, --SKIPIF--는 실행 조건에 따라 테스트를 건너뛸 수 있게 합니다.

실습 아이디어: ext/standard/tests/strings/strlen_basic.phpt를 읽고 빈 문자열과 NUL이 포함된 문자열을 왜 따로 검사하는지 설명하세요. 테스트 파일 수가 많아도 무엇을 검증하는지 구분하지 않으면 학습 효과가 줄어듭니다.

6-5. 시스템: 프로세스·스레드·요청을 구분한다

main/SAPI.h의 SG(v)는 ZTS 여부에 따라 스레드별 자원 접근과 전역 구조체 접근으로 나뉩니다. TSRM/은 이런 스레드 안전 실행 환경의 자원 관리를 지원합니다. ZTS를 활성화한다고 PHP 애플리케이션의 코드가 자동으로 여러 스레드에서 병렬 실행되는 것은 아닙니다.

실습 아이디어: FPM의 여러 작업 프로세스와 ZTS의 스레드별 전역 상태를 서로 다른 그림으로 그리세요. “요청이 여러 개다”라는 표현만으로 어떤 동시성 모델인지 판단하면 안 됩니다.

6-6. 성능: 코드 재사용과 실행 속도 향상을 분리한다

OPcache 캐시 적중, opcode 최적화, JIT, 외부 데이터베이스 응답 대기는 각각 다른 문제입니다. 정수 연산을 빠르게 해도 데이터베이스를 기다리는 시간이 대부분인 프로그램은 개선 폭이 작을 수 있습니다. benchmark/의 결과를 읽을 때도 어떤 비용을 측정하는지 먼저 확인해야 합니다.

실습 아이디어: “첫 실행 / 반복 실행”과 “연산 중심 / I/O 중심”을 두 축으로 놓고 실험 표를 설계하세요. 성능 향상 배수는 같은 조건에서 측정한 결과로 판단하세요.

7하드웨어 / 시스템 요구사항

공식 빌드 조건, 소스 실측, 학습용 예산을 구분하기

7-1. 소스 읽기와 소스 빌드는 필요한 자원이 다르다

상황필요한 것 / 예산근거와 한계
소스 읽기텍스트 편집기, Git 또는 검색 도구파일 읽기와 집계에는 PHP 실행 환경이 필요하지 않습니다.
체크아웃 내용추적 파일 내용 합계 약 139.2 MiB28,567개 추적 파일의 바이트 합계. .git, 디스크 할당량, 빌드 결과 제외.
학습용 빌드 예산CPU 2코어 이상, RAM 4 GiB, 여유 디스크 5 GiB 정도부터 계획학습용 계획 예시이며 공식 최소 사양이나 실측 보장값이 아닙니다.
기본 빌드 도구C11 컴파일러, make, pkg-config, Autoconf, Bison, re2cREADME.md, configure.ac. Windows는 별도 도구 경로입니다.
선택 확장XML·SQLite·OpenSSL·ICU 등 개발용 라이브러리활성화한 확장의 설정 검사에 따라 달라집니다. C++ 컴파일러가 필요한 확장도 있습니다.
GPU이 글에서 다루는 일반 실행·빌드 경로에 필수 아님확인한 핵심 빌드 경로는 CPU 실행기입니다. GPU 가속을 전제하지 않습니다.

README의 병렬 빌드 예시는 make -j4이고, 테스트 기본 병렬도는 탐지한 논리 CPU 중 최대 10개라고 설명합니다. run-tests.php의 기본 병렬 실행 수 계산에도 상한 10이 있습니다. 빌드 작업자와 테스트 작업자는 서로 다른 설정이며, 메모리가 부족하면 둘 다 줄여야 합니다.

debug 심벌, sanitizer, 선택 확장, 병렬도에 따라 필요한 RAM과 디스크가 늘어납니다. 위 예산만으로 전체 CI 행렬이나 모든 확장 테스트에 필요한 자원이 충분하다고 보장할 수는 없습니다.

7-2. 운영체제와 CPU에 관한 실제 코드 조건

저장소에는 Unix 계열 빌드 설정과 win32/의 Windows 구현이 함께 있습니다. CI에는 Linux x64, Alpine, ppc64 작업 등이 있으며, .github/workflows/windows-builds.yml은 태그에 맞춰 별도 Windows 빌더 워크플로를 호출합니다. CI의 플랫폼 목록은 설정된 검증 대상이며, 각 실행의 성공 여부는 CI 결과로 확인해야 합니다.

ext/opcache/config.m4는 JIT에서 x86 계열과 AArch64를 구분하고, 지원하지 않는 CPU에서는 JIT 지원을 끕니다. Apple Silicon의 ZTS 조합에는 pthread_jit_write_protect_np() 검사도 있습니다. 일반 PHP 실행 가능 여부와 JIT 사용 가능 여부는 구분해서 판단해야 합니다.

7-3. FPM 메모리를 생각하는 출발점

sapi/fpm/www.conf.in의 예시는 pm = dynamic, pm.max_children = 5, pm.start_servers = 2입니다. 이는 설정 템플릿 값으로, 내 서비스에 적절한 프로세스 수를 보장하지 않습니다.

자원 계산 비유

공유하는 교재 창고와 학생별 책상을 구분해 예산을 잡는 것과 같습니다. 대략 “공유 캐시 + 작업 프로세스별 사용량 + 운영체제·웹서버 여유”를 따져야 합니다. OPcache의 128 MiB만 보고 서버 전체 메모리를 계산할 수는 없습니다.

8직접 해볼 수 있는 실습 과제

다섯 과제: 읽기 → 예측 → 작은 테스트 → 실험 설계

아래 PHP·PHPT 코드는 별도 실습 환경에서 사용할 예시입니다. PHP 예제는 UTF-8 파일로 저장하고, PHPT 초안은 cow-example.phpt처럼 별도 디렉토리에 두세요. 검색 명령은 php/php-src 저장소 루트에서 실행합니다. 실행 과제에는 예제 문법을 지원하는 PHP 환경이 필요합니다.

과제 1 · 입문

문자열 길이의 의미를 설명하기 예상 20분

ext/standard/tests/strings/strlen_basic.phpt와 ZEND_FUNCTION(strlen)을 읽습니다. 아래 세 입력의 바이트 길이를 먼저 적고, ZSTR_LEN이 왜 그 값을 돌려주는지 설명합니다.

<?php
var_dump(strlen(""));
var_dump(strlen("가"));
var_dump(strlen("A\0B"));
// UTF-8 파일 기준 예상: int(0), int(3), int(3)

완료 기준: “눈에 보이는 글자 수”와 “저장된 바이트 수”의 차이를 말하고, NUL을 문자열 끝으로 간주하지 않는 근거를 zend_string의 길이 필드로 설명합니다.

과제 2 · 입문

echo의 경로를 파일 다섯 개로 연결하기 예상 30분

아래는 소스를 실행하지 않는 검색 명령입니다. 문법 규칙, 컴파일 함수, VM 핸들러, 출력 함수, SAPI 콜백의 연결을 메모합니다.

rg -n 'ZEND_AST_ECHO' Zend/zend_language_parser.y
rg -n 'zend_compile_echo' Zend/zend_compile.c
rg -n 'ZEND_ECHO' Zend/zend_vm_def.h
rg -n 'php_output_write|sapi_module\.ub_write' main/output.c
rg -n 'sapi_cli_ub_write' sapi/cli/php_cli.c

완료 기준: 각 연결선 위에 “AST 전달 / 명령 생성 / 문자열 전달”처럼 무엇을 넘기는지 적습니다. 함수명이 나열된 목록보다 데이터가 이동하는 그림이 목표입니다.

과제 3 · 중급

배열 대입과 객체 대입을 구분하기 예상 40분

먼저 예상 출력을 적은 뒤 SEPARATE_ARRAY와 docs/source/core/data-structures/reference-counting.rst의 분리 설명을 읽습니다.

<?php
$a = [1, 2];
$b = $a;
$b[0] = 99;
echo $a[0], ":", $b[0], "\n";

$x = (object) ["value" => 1];
$y = $x;
$y->value = 99;
echo $x->value, ":", $y->value, "\n";
// 예상: 첫 줄 1:99, 둘째 줄 99:99

완료 기준: 배열과 객체의 그림을 따로 그립니다. 배열 안에 객체를 넣으면 내부 객체까지 모두 복제되는지 추가로 생각해 보세요. 내부 참조 수의 정확한 숫자는 임시 값·최적화에 영향받을 수 있으므로 고정 출력처럼 외우지 않습니다.

과제 4 · 중급

출력 두 줄짜리 PHPT 초안 만들기 예상 30분

예를 들어 실습 디렉토리의 cow-example.phpt에 아래 형태로 초안을 작성할 수 있습니다. 원본 테스트를 바꾸지 않고 입력과 기대 출력을 한 파일에 정리하는 연습입니다.

--TEST--
배열 대입 뒤 원소 변경은 원래 배열을 바꾸지 않는다
--FILE--
<?php
$a = [1];
$b = $a;
$b[0] = 2;
var_dump($a[0]);
var_dump($b[0]);
?>
--EXPECT--
int(1)
int(2)

완료 기준: 기대 출력의 각 줄이 무엇을 검증하는지 설명합니다. run-tests.php는 PHP 인터프리터가 필요한 실행기입니다. 실제 통과 여부는 사용하는 PHP 환경에서 실행해 확인하세요.

과제 5 · 고급

OPcache와 JIT를 분리한 실험 설계하기 예상 60분 이상

persistent_compile_file()의 원래 컴파일러 위임 경로를 찾은 뒤, 아래 실험표를 작성합니다. 별도 실행 환경을 마련하기 전에는 예상과 관측 계획만 적습니다.

조건비교할 관측통제할 항목
OPcache 끔 / 켬첫 요청과 반복 요청의 컴파일 관련 비용같은 코드·버전·SAPI, 캐시 상태
JIT 끔 / 켬연산 중심 코드와 I/O 중심 코드의 차이OPcache 상태, JIT 설정, 준비 실행
CLI / FPM프로세스 수명과 캐시 재사용 조건opcache.enable_cli, 요청 횟수

완료 기준: 설정값과 실제 활성 상태를 구분해 기록하고, 반복 실행 분포와 환경을 함께 남깁니다. “더 빠르다”만 쓰기보다 어떤 비용이 줄었는지 소스 경로와 연결해 설명합니다.

9관련 기술 심화 학습 로드맵

주당 3~5시간을 가정한 6주 읽기 계획

PHP 문법으로 함수·배열·객체를 작성해 본 독자를 가정합니다. C를 처음 본다면 1주차를 늘려도 좋습니다. 각 주의 목표는 파일 통독이 아니라 하나의 동작을 근거와 함께 설명하는 산출물을 만드는 것입니다.

주차학습 주제와 읽을 소스남길 결과물
1주차C 포인터·구조체·union·매크로
Zend/zend_types.h, main/SAPI.h
zval 그림과 함수 포인터 표. 주소 전달과 값 전달을 구분한 메모.
2주차토큰·문법·AST
Zend/zend_language_scanner.l, Zend/zend_language_parser.y
출력문 하나의 토큰 목록과 AST. 파서의 책임을 한 문단으로 설명.
3주차opcode와 VM
Zend/zend_compile.c, Zend/zend_vm_def.h
ZEND_AST_ECHO에서 출력 콜백까지 이어지는 흐름도.
4주차참조 수·COW·순환 참조
Zend/zend_types.h, Zend/zend_gc.c
배열 대입과 객체 대입 비교 그림. 작은 PHPT 초안.
5주차확장 API·요청 수명
ext/json/json.c, main/main.c, FPM 진입점
모듈 초기화와 요청 초기화를 구분한 표. 인자 검사 경로 메모.
6주차OPcache·빌드·CI
ext/opcache/ZendAccelerator.c, configure.ac, CI YAML
캐시 적중·실패 분기와 실험 계획. debug/ZTS/sanitizer의 역할 비교.

9-1. 다음으로 뻗어 갈 세 갈래

학습의 진도는 읽은 줄 수보다, 입력 하나가 결과가 되는 과정을 얼마나 정확히 설명하는지로 확인하세요.

매주 해 볼 점검
근거 없는 화살표를 하나씩 없애기

흐름도의 화살표마다 실제 함수명 또는 자료형을 적습니다. 연결을 찾지 못했다면 “미확인”으로 남기고 다음 검색 대상을 정하세요. 추측을 사실처럼 이어 붙이지 않는 습관도 소스 분석의 중요한 기술입니다.

10핵심 키워드 사전

이 글에서 다시 찾아볼 최소한의 용어
용어쉬운 설명
php-srcPHP 프로그램을 읽고 실행하는 공식 구현의 소스 저장소입니다.
Zend EnginePHP의 문법·컴파일·실행·자료형 등을 담당하는 핵심 엔진입니다.
SAPI엔진을 터미널·웹서버 등 실행 환경과 연결하는 인터페이스입니다.
CLI터미널의 명령으로 프로그램을 실행하는 환경입니다.
FPMFastCGI 요청을 처리하는 PHP 작업 프로세스를 관리하는 실행 환경입니다.
FastCGI웹서버와 별도 실행 프로세스가 요청·응답을 주고받는 규약입니다.
어휘 분석소스의 글자를 변수·기호·키워드 같은 토큰으로 나누는 단계입니다.
파서토큰의 문법 관계를 확인하고 의미 구조를 만드는 부품입니다.
AST문장의 의미 관계를 나무 모양으로 표현한 중간 자료구조입니다.
opcodeVM이 처리하는 내부 명령입니다. CPU 기계어와는 다릅니다.
zend_op_array컴파일된 명령과 실행에 필요한 정보를 담는 구조체입니다.
VMopcode를 읽어 실제 동작을 수행하는 엔진 내부 실행기입니다.
zvalPHP 값의 저장 공간과 타입 정보를 함께 담는 기본 그릇입니다.
zend_string문자열 바이트와 길이, 관리 정보를 함께 보관하는 구조체입니다.
HashTable키로 값을 찾는 핵심 자료구조입니다. PHP 배열 등의 구현에 쓰입니다.
참조 수동적으로 저장된 값을 몇 곳에서 사용 중인지 추적하는 관리 정보입니다.
COW값을 일단 공유하고, 변경 때문에 필요해질 때 복사하는 방식입니다.
순환 참조값들이 서로를 가리켜 단순 참조 수만으로는 해제를 판단하기 어려운 관계입니다.
OPcachePHP 스크립트의 컴파일 결과를 재사용하는 계층입니다.
JIT실행 중 선택한 코드를 CPU가 직접 실행할 기계어로 만드는 기술입니다.
훅기존 처리 경로에 다른 처리를 연결할 수 있게 마련한 지점입니다.
MINIT / RINIT확장의 모듈 초기화와 요청별 초기화입니다. 실행되는 수명 범위가 다릅니다.
ZTS / NTS스레드 안전 자원 관리 사용 여부를 구분하는 빌드 방식입니다.
PHPT테스트 코드·설정·기대 출력 등을 구획으로 담는 PHP 테스트 파일 형식입니다.
ASan / UBSan메모리 오류와 정의되지 않은 C/C++ 동작 등을 탐지하는 개발용 검사 도구입니다.
shallow clone과거 이력을 제한해서 가져온 복제본입니다. 전체 커밋 수를 세는 데 쓸 수 없습니다.

11참고 링크

공식 안내, 고정된 소스 근거, 집계 방법과 분석 범위

11-1. 공식 저장소와 학습 안내

11-2. 본문의 소스 근거

아래 소스 링크는 모두 커밋 02207318을 가리킵니다. 기본 브랜치는 이후에 바뀔 수 있으므로 본문의 구현 설명은 이 커밋을 기준으로 읽으세요.

현재 상태 (2026-10-23 확인)

항목확인한 값출처 / 범위
저장소 / 설명php/php-src / The PHP Interpreter2026-10-23 GitHub API 기준
스타 / 포크41,332 / 8,3302026-10-23 GitHub API 기준
생성일2011-06-16GitHub 저장소 생성일. PHP 언어의 탄생일을 뜻하지 않습니다.
주 언어C2026-10-23 GitHub API 기준. 소스 집계도 참고.
최신 릴리스php-8.5.12 · 2026-10-212026-10-23 GitHub API 기준. 본문의 개발 소스와 별개입니다.
최근 push2026-10-232026-10-23 GitHub API 기준
최근 커밋02207318 · 2026-10-23
Merge branch 'PHP-8.6'
커밋 기록 시각 08:27:34 +02:00.
소스의 버전8.7.0-dev · PHP_VERSION_ID 80700configure.ac, main/php_version.h
라이선스BSD-3-Clause2026-10-23 GitHub API 기준. README와 LICENSE도 같은 라이선스를 명시합니다. 번들 코드에는 별도 고지가 있을 수 있습니다.
C / 헤더.c 1,097개 · 1,346,172줄
.h 1,140개 · 680,222줄
합계 2,026,394줄. 생성 코드·번들 라이브러리 포함.
C++ / PHP.cpp 74개 · 20,638줄
.php 202개 · 64,368줄
확장자별 물리 줄 수. PHP 도구와 stub 등 포함.
테스트 파일.phpt 23,655개 · 1,340,039줄설명·PHP 코드·기대 출력 등을 포함한 전체 파일 줄 수.
문법 / 빌드.y 3,004줄 · .l 4,153줄
.m4 21,307줄 · .sh 12,415줄
각 확장자별 합계. 생성 결과와 원본을 중복 제거하지 않았습니다.
전체 파일 내용28,567개 · 145,948,959바이트
약 139.2 MiB
Git 추적 파일 내용 합계. 언어별 줄 수 표는 전체 확장자를 망라하지 않습니다.

11-3. 코드 규모 집계 기준

집계 대상은 git ls-files에 나오는 추적 파일이며, 확장자별로 묶어 줄바꿈 바이트를 셉니다. 빈 줄과 주석을 포함하는 wc -l 방식의 집계입니다. 아래 코드는 저장소 루트에서 같은 기준으로 파일 내용을 읽어 집계합니다.

python3 - <<'PY'
from pathlib import Path
from collections import defaultdict
import subprocess

root = Path('.')
paths = subprocess.check_output(['git', 'ls-files', '-z'])
stats = defaultdict(lambda: [0, 0])
for name in paths.split(b'\0'):
    if not name:
        continue
    p = root / name.decode()
    if p.is_file():
        stats[p.suffix][0] += 1
        stats[p.suffix][1] += p.read_bytes().count(b'\n')
for ext in ['.c', '.h', '.cpp', '.php', '.phpt', '.y', '.l', '.m4', '.sh']:
    print(ext, *stats[ext])
PY

이 수치는 사람이 직접 작성한 유효 코드만 세는 SLOC가 아닙니다. 자동 생성된 VM 코드, 문자 데이터, 외부에서 가져온 코드도 포함합니다. 코드 줄 수는 읽을 범위를 가늠하는 자료이며, 품질이나 성능을 증명하는 점수는 아닙니다.

이 자료와 소스를 쓰기 전에 알아둘 한계
구현 설명의 기준과 실제 실행 환경을 구분하세요

구현 설명은 커밋 02207318 기준입니다. 파일 규모만으로 전체 커밋 수·기여자별 비중·기간별 변경 추세를 알 수는 없습니다. 스타·포크·최신 릴리스는 2026-10-23 GitHub 기준이며, 선정 근거는 같은 날짜의 TrendShift Daily #23입니다. 이런 지표만으로 트렌딩의 직접 원인이나 특정 기능의 성능 향상을 단정할 수는 없습니다.

예제 출력은 소스와 언어 의미에 근거한 예상이며, 하드웨어 예산은 학습용 계획 예시입니다. 다이어그램과 발췌는 주요 경로를 줄여 설명합니다. 실제 오류 처리·플랫폼 분기 전체를 대체하지 않습니다.

현재 개발 소스는 안정 릴리스 8.5.12와 다릅니다. 이 글을 실제 서비스 설정에 적용할 때는 사용하는 배포판·버전·SAPI·활성 확장의 설정을 별도로 확인해야 합니다. API 이름과 설정 기본값도 커밋 02207318 기준입니다.