《手把手教你编写PHP编译器》--准备工作

准备工作

在写代码之前,我们很有必要先把编译C++代码的工作做好。主要涉及到以下几个方面:

1
2
1. 编写CMakeLists
2. 编写一个编译的脚本

编写CMakeLists

因为CMakeLists.txt的内容比较简单,所以我直接贴出我们的CMakeLists.txt文件的内容:

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
cmake_minimum_required(VERSION 3.4)
project(yaphp)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_COMPILER clang++)

find_package(FLEX REQUIRED)
set(FlexOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.cc)
if(FLEX_FOUND)
add_custom_command(
OUTPUT ${FlexOutput}
COMMAND ${FLEX_EXECUTABLE}
--outfile=${FlexOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.l
COMMENT "Generating zend_language_scanner.cc"
)
endif()

find_package(BISON REQUIRED)
set(BisonOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.cc)
if(BISON_FOUND)
add_custom_command(
OUTPUT ${BisonOutput}
COMMAND ${BISON_EXECUTABLE}
--defines=${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.h
--output=${BisonOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.y
COMMENT "Generating zend_language_parser.cc"
)
endif()

add_executable(yaphp
${FlexOutput}
${BisonOutput}
)
include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_options(yaphp PUBLIC ${CMAKE_CXX_FLAGS} -Wall -Wno-deprecated-register -O0 -g)

message(STATUS "summary of build options:
Install prefix: ${CMAKE_INSTALL_PREFIX}
Target system: ${CMAKE_SYSTEM_NAME}
Compiler:
CXX compiler: ${CMAKE_CXX_COMPILER}
")

我们来讲一下核心的东西,其他不清楚的地方,可以网上搜一下。

首先来看这段代码:

1
project(yaphp)

我们把我们的这个项目叫做yaphp,即表示Yet another php

然后是这段代码:

1
2
3
4
5
6
7
8
9
10
11
find_package(FLEX REQUIRED)
set(FlexOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.cc)
if(FLEX_FOUND)
add_custom_command(
OUTPUT ${FlexOutput}
COMMAND ${FLEX_EXECUTABLE}
--outfile=${FlexOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_scanner.l
COMMENT "Generating zend_language_scanner.cc"
)
endif()

这段代码做的事情是,通过flex,让zend_language_scanner.l文件生成zend_language_scanner.cc文件(如果不清楚zend_language_scanner.l的小伙伴不用着急,我们后面会讲)。并且,我们可以看到,我们把zend_language_scanner.l文件放在了Zend目录下,这实际上是和php-src(即php解释器)的一致的。

然后是这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
find_package(BISON REQUIRED)
set(BisonOutput ${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.cc)
if(BISON_FOUND)
add_custom_command(
OUTPUT ${BisonOutput}
COMMAND ${BISON_EXECUTABLE}
--defines=${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.h
--output=${BisonOutput}
${CMAKE_SOURCE_DIR}/src/Zend/zend_language_parser.y
COMMENT "Generating zend_language_parser.cc"
)
endif()

这段代码做的事情是,通过bison,让zend_language_parser.y文件生成zend_language_parser.cc文件和zend_language_parser.h文件(如果不清楚zend_language_parser.y的小伙伴不用着急,我们后面会讲)。

然后是这段代码:

1
2
3
4
add_executable(yaphp
${FlexOutput}
${BisonOutput}
)

表示,我们要把${FlexOutput}${BisonOutput}编译成yaphp可执行文件。

OK,按照这个CMakeLists.txt的意思,我们来创建对应的文件。首先是文件src/Zend/zend_language_scanner.l

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
%option noyywrap
%option nounput
%option noinput
%{
#include "zend_language_parser.h"
%}

%%
echo {
return T_ECHO;
}

[;(),+*/-] return *yytext;

[0-9]+ {
yylval = atoi(yytext);
return T_NUMBER;
}

[\t\n ]+ /* ignore \t, \n, whitespace */

%%

然后是文件src/Zend/zend_language_parser.y

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
%{
#include <stdio.h>
#include <string.h>

extern int yylex(void);
extern int yyparse(void);
extern FILE *yyin;
extern int yylineno;

int yywrap()
{
return 1;
}

void yyerror(const char *s)
{
printf("[error] %s, in line %d\n", s, yylineno);
}

int main(int argc, char const *argv[])
{
const char *file = argv[1];
FILE *fp = fopen(file, "r");

if(fp == nullptr)
{
printf("cannot open %s\n", file);
return -1;
}

yyin = fp;
yyparse();

return 0;
}
%}

%token T_ECHO T_NUMBER

%%

statement: %empty
| T_ECHO expr { printf("%d\n", $2); }
;

expr: %empty
| T_NUMBER {$$ = $1;}
;

%%

然后,我们创建文件tests/test1.php

1
echo 1

编写编译的脚本

我们创建文件rebuild.sh

1
2
3
4
#!/bin/bash
__DIR__=$(cd "$(dirname "$0")" || exit 1; pwd); [ -z "${__DIR__}" ] && exit 1

cd "${__DIR__}" && ./clean.sh && mkdir -p build && cd build && cmake .. && make

这段代码很简单,就是先调用clean.sh脚本做一些清理工作,然后调用cmake来生成Makefile,然后调用make来编译代码,生成yaphp

然后创建文件tools/cleaner.sh

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
#!/bin/bash
__DIR__=$(cd "$(dirname "$0")" || exit 1; pwd); [ -z "${__DIR__}" ] && exit 1

error(){ echo "[ERROR] $1"; exit 1; }
success(){ echo "[SUCCESS] $1"; exit 0; }
info(){ echo "[INFO] $1";}

workdir="$1"

if ! cd "${workdir}"; then
error "Cd to ${workdir} failed"
fi

info "Scanning dir \"${workdir}\" ..."

if [ ! -f "./Makefile" ] && [ ! -f "./CMakeLists.txt" ]; then
error "Non-project dir ${workdir}"
fi

info "CMake build dir will be removed:"

rm -rf -v ./build

info "Following files will be removed:"

find ${workdir}/src/Zend -name zend_language_scanner.cc -print0 | xargs -0 rm -f -v
find ${workdir}/src/Zend -name zend_language_parser.h -print0 | xargs -0 rm -f -v
find ${workdir}/src/Zend -name zend_language_parser.cc -print0 | xargs -0 rm -f -v

success "Clean '${workdir}' done"

这个脚本会清理掉cmake生成的一系列文件。

然后创建文件clean.sh

1
2
3
4
#!/bin/bash
__DIR__=$(cd "$(dirname "$0")" || exit 1; pwd); [ -z "${__DIR__}" ] && exit 1

"${__DIR__}"/tools/cleaner.sh "${__DIR__}"

OK,现在,所以的事情都做好了,我们只需要执行脚本rebuild.sh

1
2
3
4
5
./rebuild.sh

# 省略其他的输出
[100%] Linking CXX executable yaphp
[100%] Built target yaphp

现在,你将会在目录build下面看到编译好的yaphp。并且,细心的话,你会发现,在目录src/Zend下面,生成了文件zend_language_scanner.cczend_language_parser.hzend_language_parser.cc

现在,让我们执行这个yaphp

1
2
./build/yaphp tests/test1.php
1

我们将会看到,1被打印了出来。