前言

由于国内恶劣的网络环境,作为一名频繁访问外网的Web开发者,不得不自建一个VPN服务,以下简单介绍Shadowsocks的搭建过程。

单纯的ss流量会迅速被gfw识别到并屏蔽,因此使用ss时需要增加obfs用以混淆欺骗gfw,可以极大程度减小被屏蔽的风险。当然,如果你用的是比较出名的服务器供应商,如bandwagon的服务器,那么被屏蔽的机会也很大。

服务端使用的一键安装脚本twist由于开发者已经放弃维护长时间,目前只有参考作用,如果你有耐心,也可以自己修改后再使用,也就跟下面的服务端搭建教程一样。搞完是真的累,太渣了,太需要提高自己的编程水平,写一个一体化的软件包才行

服务端

twist脚本

服务端使用的是twist脚本,里面集成了Shadowsocks+simple-obfs+fail2ban以及相关依赖包,使用shell编写,感兴趣的可以看看源代码,下载地址如下

1
https://github.com/forestbat/Twist

下载后,脚本添加执行权限

1
chmod +x twist

切换到root账户,twist执行需要

1
su - root

修改twist

twist文件里面有一些软件需要删掉/更换,有些则需要手动安装,先手动执行一遍twist脚本./twist install,确认有哪些错误,然后选择执行下面的命令

  1. 修改dependenciesinstall函数
1
2
3
4
5
6
7
# 不同的系统不一样,不要全改
# 1. 新增依赖包,如libcork-dev
# 2. 名称更换,如python-pip改为python3-pip
apt install python3-dev python3-pip python3-setuptools python3-m2crypto libcork-dev

# 将pip改为pip3,可能需要使用pip安装m2crypto
pip3 install qrcode
  1. 修改sslibevinstall函数
1
2
# github的包命名改了,解压后变成了libsodium-stable
tar zxf libsodium-${libsodiumver}.tar.gz && mv libsodium-stable libsodium-${libsodiumver}

或者删掉libsodium这段,直接手动编译安装,如下

1
2
3
4
5
wget https://github.com/jedisct1/libsodium/releases/download/1.0.19-RELEASE/libsodium-1.0.19.tar.gz
tar zxf libsodium-1.0.19.tar.gz
cd libsodium-1.0.19
./configure
make && make install
  1. 修改sslibevinstall函数
1
2
3
4
5
6
7
# 在 tar zxf ${ssobfsver}.tar.gz 这一行后面修改添加代码,因为obfs也有很长时间没有更新,导致在新的编译器下报错
if [ "$ssobfstag" == "v0.0.5" ]; then
# patch
sed -i -e "249s/$/ else {result = OBFS_ERROR;}/" /tmp/${ssobfsver}/src/obfs_http.c
sed -i -e '246d' /tmp/${ssobfsver}/src/obfs_http.c
# patch end
fi
  1. 修改sslibevinstallerrsslibevinstall函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# sslibevinstallerr函数如下
# 删掉mbedtls相关包,与libsodium类似,因为命名问题我直接绑定版本了
rm -rf libsodium-${libsodiumver}.tar.gz libsodium-${libsodiumver} ${mbedtlsver}.tar.gz ${mbedtlsver} ${sslibevver}.tar.gz $sslibevver ${ssobfsver}.tar.gz $ssobfsver

# sslibevinstall函数如下
# 搜索与mbedtlstag有关的代码段,修改绑定版本v2.28.1
# ldconfig
[ -z "$mbedtlstag" ] && mbedtlstag="v2.28.1"
mbedtlsver="mbedtls-$(echo ${mbedtlstag} | sed -e 's/^[a-zA-Z]//g')"
wget -t 3 -T 30 -nv -O ${mbedtlsver}.tar.gz https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/${mbedtlstag}.tar.gz
[ "$?" != "0" ] && sslibevinstallerr "mbedtls-$(echo ${mbedtlstag} | sed -e 's/^[a-zA-Z]//g')"
[ -d ${mbedtlsver} ] && rm -rf "${mbedtlsver}"
tar zxf ${mbedtlsver}.tar.gz
pushd $mbedtlsver
make SHARED=1 CFLAGS=-fPIC "-j$((MAKECORES+1))" && make DESTDIR=/usr install || sslibevinstallerr "mbedtls-${mbedtlstag} | sed -e 's/^[a-zA-Z]//g'" err
popd
ldconfig
rm -rf libsodium-${libsodiumver}.tar.gz libsodium-${libsodiumver} mbedtls-${mbedtlsver}.tgz mbedtls-${mbedtlsver}

安装完毕

执行修改后的twist脚本

1
./twist install

如果twist安装成功,此时命令行下将会展示ss服务的二维码以及url,手机端可以用小火箭扫描,桌面端可以复制url导入

客户端

安装软件

1
2
# obfs混淆ss流量;privoxy将http流量转换为socks5流量
sudo apt install shadowsocks-libev simple-obfs privoxy

ss配置

修改ss配置,配置文件可以是/etc/shadowsocks-libev/config.json,也可以是/etc/shadowsocks-libev/config-obfs.json

1
sudo vi /etc/shadowsocks-libev/config.json

按下方模板配置:

1
2
3
4
5
6
7
8
9
10
11
{
"server":["server_ip"],
"mode":"tcp_and_udp",
"server_port":443,
"local_port":1080,
"password":"server_password",
"timeout":86400,
"method":"xchacha20-ietf-poly1305",
"plugin":"obfs-local",
"plugin_opts":"obfs=tls;obfs-host=mzstatic.com;obfs-uri=/"
}

执行下面的命令,确认软件可正常运行

1
2
3
4
5
6
7
8
# 查看输出确认无异常报错
sudo ss-local -c /etc/shadowsocks-libev/config.json

# 1080端口
sudo netstat -tnlp | grep 1080

# 如果发现ss服务未能正常启动,查看status信息
sudo systemctl status shadowsocks-libev.service

修改systemd配置文件

1
sudo vi /usr/lib/systemd/system/shadowsocks-libev.service

ss-server改为ss-local,因为我们修改的是shadowsocks-libev.service,配置文件就是config.json,这里就不需要再修改什么

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
#  This file is part of shadowsocks-libev.
#
# Shadowsocks-libev is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This file is default for Debian packaging. See also
# /etc/default/shadowsocks-libev for environment variables.

[Unit]
Description=Shadowsocks-libev Default Client Service
Documentation=man:shadowsocks-libev(8)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
DynamicUser=true
EnvironmentFile=/etc/default/shadowsocks-libev
LimitNOFILE=32768
ExecStart=/usr/bin/ss-local -c $CONFFILE $DAEMON_ARGS

[Install]
WantedBy=multi-user.target

重启ss服务

1
2
sudo systemctl daemon-reload
sudo systemctl restart shadowsocks-libev

privoxy配置

修改配置文件/etc/privoxy/config

1
sudo vi /etc/privoxy/config

修改listen-address,因为有时我会将地址共享出来给局域网其他设备使用,所以我一般会将127.0.0.1改为0.0.0.0。在末尾添加forward-socks5指令,将所有http流量转发到1080端口

1
2
listen-address  0.0.0.0:8118
forward-socks5 / 127.0.0.1:1080 .

重启privoxy服务

1
sudo systemctl restart privoxy

防火墙开放8118端口访问(可选,如果没有打算给局域网内的其他机器使用就不要打开)

1
2
sudo ufw allow 8118/tcp
sudo ufw reload

最后验证

配置环境变量http_proxy以及https_proxy,访问外网地址,只要有html数据流输出即可

1
2
3
export http_proxy=http://127.0.0.1:8118;export https_proxy=http://127.0.0.1:8118;

curl -i https://www.google.com/

参考文档

Twist脚本github地址
Debian11下编译安装Shadowsocks-libev
Linux中使用ShadowSocks+Privoxy代理

以下简单介绍如何在C语言层面使用PHP类

对象

创建一个PHP对象,对象类似关联数组,对象之上可关联任意多个函数

1
2
3
4
5
6
7
PHP_FUNCTION(makeObject) {
object_init(return_value);

// 添加属性
zend_update_property_string(NULL, return_value, "prop1", strlen("prop1"), "val1");
zend_update_property_long(NULL, return_value, "prop2", strlen("prop2"), 123);
}

调用函数并打印结果var_dump(makeObject());,输出如下

1
2
3
4
5
6
object(stdClass)#1 (2) {
["prop1"]=>
string(3) "val1"
["prop2"]=>
int(123)
}

创建一个类模板

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
// class定义存储于zend_class_entry
zend_class_entry *test_ce_myclass;

static const zend_function_entry test_methods[] = {
// 类方法使用宏指令PHP_ME
// public function hello()
PHP_ME(MyClass, hello, NULL, ZEND_ACC_PUBLIC)
// 与函数定义相同
PHP_FE_END
};

// 主菜,注册并加载类
static void test_init_myclass()
{
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, "MyClass", test_methods);
// 注册MyClass类
test_ce_myclass = zend_register_internal_class(&ce);
// 添加属性/常量等
// public $success = true;
zend_declare_property_bool(test_ce_myclass, "success", sizeof("success")-1, 1, ZEND_ACC_PUBLIC);
}

// 类方法hello
PHP_METHOD(MyClass, hello)
{
RETURN_STRING("hello");
}

其中,函数test_init_myclass的最后一个参数ZEND_ACC_PUBLIC为访问控制标记之一公共访问,常用的访问控制标记还有以下几个

1
2
3
4
5
6
7
ZEND_ACC_STATIC
ZEND_ACC_PUBLIC
ZEND_ACC_PROTECTED
ZEND_ACC_PRIVATE
ZEND_ACC_CTOR
ZEND_ACC_DTOR
ZEND_ACC_DEPRECATED

一个class定义注册相关逻辑已经完成,要在PHP中使用类MyClass还需要在模块初始化MINIT中添加运行test_init_myclass以加载类MyClass

1
2
3
4
5
PHP_MINIT_FUNCTION(test)
{
test_init_myclass();
return SUCCESS;
}

编译test模块并开启后,运行var_dump(new MyClass());,将得到以下类似输出

1
2
3
4
object(MyClass)#1 (1) {
["success"]=>
bool(true)
}

我们也可以直接在C层面初始化并生成一个实例化类对象,增加一个工厂方法factory

1
2
3
4
5
6
7
8
9
10
static const zend_function_entry test_methods[] = {
PHP_ME(MyClass, hello, NULL, ZEND_ACC_PUBLIC)
PHP_ME(MyClass, factory, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_FE_END
};

PHP_METHOD(MyClass, factory)
{
object_init_ex(return_value, test_ce_myclass);
}

此时,我们可以使用MyClass::factory()获取一个新的MyClass对象

如果需要对MyClass进行一些操作,像在PHP使用构造方法,在test_methods里添加__construct,如此,PHP在new MyClass()将自动调用构造方法__construct

1
2
3
4
5
6
7
8
9
10
11
12
static const zend_function_entry test_methods[] = {
// ZEND_ACC_CTOR
PHP_ME(MyClass, __construct, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_CTOR)
PHP_ME(MyClass, hello, NULL, ZEND_ACC_PUBLIC)
PHP_ME(MyClass, factory, NULL, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
PHP_FE_END
};

PHP_METHOD(MyClass, __construct)
{

}

调用类方法

上面,虽然对象在new时会自动调用__construct函数进行初始化,但factory不会自动调用构造函数,仅返回包含默认值的新对象,为此,我们需要在factory内调用构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "zend_interfaces.h"

PHP_METHOD(MyClass, factory)
{
zval *myzval;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_ZVAL(myzval)
ZEND_PARSE_PARAMETERS_END();

object_init_ex(return_value, test_ce_myclass);

// zend_call_method,其他的还有
// zend_call_method_with_0_params
// zend_call_method_with_1_params
// zend_call_method_with_2_params
// ZEND_API zval* zend_call_method(zval *object_pp, zend_class_entry *obj_ce, zend_function **fn_proxy, const char *function_name, size_t function_name_len, zval *retval, int param_count, zval* arg1, zval* arg2);
zend_call_method(return_value, test_ce_myclass, NULL, "__construct", sizeof("__construct")-1, NULL, 1, myzval, NULL);
}

this

先前我们已经了解函数返回值return_value的使用,现在,我们来看如何在方法内访问$this,PHP提供getThis()函数

1
2
3
4
5
6
7
8
9
10
11
PHP_METHOD(MyClass, __construct)
{
char *msg;
size_t msg_len;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_STRING(msg, msg_len)
ZEND_PARSE_PARAMETERS_END();

zend_update_property_string(test_ce_myclass, getThis(), "msg", sizeof("msg")-1, msg);
}

关联结构体

我们构建一个结构体,这个结构体在PHP是无法访问的,但可以在扩展内访问对该结构体

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
// 还不是特别了解
typedef struct _test_struct {
zend_object std;
int unknown_id;
char *unknown_str;
} test_struct;

static zend_object *create_test_struct(zend_class_entry *class_type) {
test_struct *intern;

intern = ecalloc(1, sizeof(test_struct) + zend_object_properties_size(class_type));

zend_object_std_init(&intern->std, class_type);
object_properties_init(&intern->std, class_type);

intern->std.handlers = zend_get_std_object_handlers();

return &intern->std;
}

static void free_test_struct(void *object) {
test_struct *secrets = (test_struct*)object;
if (secrets->unknown_str) {
efree(secrets->unknown_str);
}
efree(secrets);
}

访问结构体test_struct

1
2
3
4
5
6
7
8
PHP_METHOD(MyClass, attachStruct) {
test_struct *secrets;

//
secrets = (test_struct*)getThis();

RETURN_LONG(secrets->unknown_id);
}

异常

PHP的异常继承自Exception,所以下面除了新增一个异常类,还涉及了类的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <zend_exceptions.h>

zend_class_entry *test_ce_exception;

static void test_init_exception() {
zend_class_entry ce;
// 设置第三个参数为NULL,继承Exception所有方法,无自定义行为
INIT_CLASS_ENTRY(ce, "MyException", NULL);
//
test_ce_exception = zend_register_internal_class_ex(&ce, (zend_class_entry*)zend_exception_get_default());
}

PHP_MINIT_FUNCTION(test)
{
test_init_myclass();
//
test_init_exception();
return SUCCESS;
}

在方法/函数中抛出异常

1
2
3
PHP_METHOD(MyClass, throwExcept) {
zend_throw_exception(test_ce_exception, "custom exception throw", 1024);
}

完整代码

php_ext_tutorial

参考文档

PHP Extensions Made Eldrich: Classes

从19年开始,ss流量已经能被gfw识别并屏蔽,原本打算切换到v2ray,但配置混淆需要域名反而会暴露出更多信息,此外,网上一篇文章指出v2ray开发者失联的同时一项v2ray流量识别的专利被注册,因此决定放弃v2ray并转向另一个被linus盛赞的wireguard。

wireguard是非常简单、现代化、快速的vpn,使用最新的加密技术,udp传输,支持ip漫游等。wireguard没有服务端、客户端的区分,每一台设备都是一个peer

安装

添加wireguard的PPA源

1
2
3
sudo add-apt-repository ppa:wireguard/wireguard
sudo apt update
sudo apt install wireguard

wireguard作为linux内核模块方式加载,内核更新wireguard也会自动重新编译,重启系统或使用modprobe开启模块

1
sudo modprobe wireguard

检查wireguard模块是否成功加载,成功输出与下面类似

1
2
3
4
vagrant@bionic:~$ lsmod | grep wireguard
wireguard 221184 0
ip6_udp_tunnel 16384 1 wireguard
udp_tunnel 16384 1 wireguard

生成keys

wireguard提供了两个命令行工具wg以及wg-quick,使用wg生成公钥和私钥

1
2
umask 077
wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey

类似ssh,需要为每台设备生成一对publickey/privatekey

配置

我们需要生成一个虚拟网卡,名称可以按个人喜好选择,这里选择用wg0,IP网段使用10.10.0.1/24,确保该网段没有被你的家庭或公司网络使用,编辑文件/etc/wireguard/wg0.conf

1
2
3
4
5
6
7
8
[Interface]
PrivateKey = <your server private key here>
Address = 10.10.0.1/24
Address = fd86:ea04:1111::1/64
SaveConfig = true
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o enp0s3 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o enp0s3 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o enp0s3 -j MASQUERADE
ListenPort = 51820

PrivateKey填上一步生成的私钥,填入你选择的Address,这里配置了两个IP4/6的地址,enp0s3是你服务器主要的网络设备,使用ifconfigip a查看你的主设备名,ListenPort默认为51820,建议修改

要注意的是SaveConfig设置,wireguard配置后不需要restartreload,使用wg命令添加peer即可立即生效。但是,这些操作都是直接写入到内存中而不是磁盘,因此如果你重启系统或服务,那么你就会失去添加的配置,所以这里设置为true,自动保存配置。但这个配置有时候也会让人非常困扰,你可能添加配置后重启服务,然后你发现添加的配置不见了,需要先停止服务再添加配置最后重启。

添加网络配置,使得内核可以正确路由我们的NAT流量

1
2
3
4
5
sudo bash -c 'cat << EOF >> /etc/sysctl.conf
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF'
sudo sysctl -p

运行命令,开启wg0设备

1
sudo wg-quick up wg0

如果一切顺利,那么运行下面命令你将看到类似的输出

1
2
3
4
5
vagrant@bionic:~$ sudo wg
interface: wg0
public key: Td4IBZg2s2bMSjewD0h4oBhlbaJteygxjT83xeM7uTY=
private key: (hidden)
listening port: 51820

客户端配置

客户端配置与服务端类似

1
2
3
4
5
6
7
8
9
10
11
[Interface]
Address = 10.10.0.2/32
Address = fd86:ea04:1111::2/128
SaveConfig = true
PrivateKey = <your client private key here>
DNS = 8.8.8.8

[Peer]
PublicKey = <your server public key here>
Endpoint = <your server public ip>:51820
AllowedIPs = 0.0.0.0/0, ::/0

填写服务器的publickey、ip、端口,配置0.0.0.0/0, ::/0将所有流量传输到服务端

在服务端添加客户端配置

1
sudo wg set wg0 peer <client-public-key> allowed-ips 10.10.0.2/32,fd86:ea04:1111::2/128

以上的操作针对的是linux系统,官方推出了可用的iOS/MacOS/Android/Windows图形界面软件,其中有一个选项要注意的是On-Demand,有EthernetWi-Fi选项,如果勾选,那么会在选中网络中会自动开启vpn

设置开机启动

如果你的系统使用systemd,如ubuntu,设置wireguard开机启动命令如下

1
systemctl enable wg-quick@wg0

开放防火墙

UFW

1
sudo ufw allow 51820/udp

参考文档

WireGuard: fast, modern, secure VPN tunnel
How to setup your own VPN server using WireGuard on Ubuntu
Setting Up PiHole, Wireguard VPN server and Client (Ubuntu Server)

本文将简单介绍资源类型、资源的创建、访问、销毁操作。不再建议使用资源类型,使用类更为合适

资源类型

zval可以表示大部分的PHP数据类型,但有一样不能很好的表示其结构:指针。由于不透明的结构、无法使用传统运算符进行操作等,使得指针在PHP的表示变得困难。因此PHP用一个特殊的标记表示指针:资源,为了使资源标记具有意义,必须先注册到zend engine才能使用。

在头文件定义结构体php_test_person以及资源名称,放置在#define语句后,PHP_MINIT_FUNCTION(test);之前

1
2
3
4
5
6
typedef struct {
zend_string *name;
zend_long age;
} php_test_person;

#define PHP_TEST_PERSON_RES_NAME "Person Data"

源文件定义le_*全局变量,MINIT阶段注册,用于获取资源类型、字面意义名称、析构函数

1
2
3
4
5
6
int le_test_person;

PHP_MINIT_FUNCTION(test)
{
le_test_person = zend_register_list_destructors_ex(NULL, NULL, PHP_TEST_PERSON_RES_NAME, module_number);
}

初始化资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PHP_FUNCTION(test_person_new)
{
php_test_person * person;
zend_string * name;
zend_long age;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_STR(name);
Z_PARAM_LONG(age);
ZEND_PARSE_PARAMETERS_END();

if (age < 0 || age > 255) {
php_error_docref(NULL, E_WARNING, "Nonsense age (%ld) given, person resource not created.", age);
RETURN_FALSE;
}

person = emalloc(sizeof(php_test_person));
person->name = zend_string_copy(name); // estrndup + zend_string => zend_string_copy
person->age = age;

RETURN_RES(zend_register_resource(person, le_test_person));
}

函数接收参数name以及age,参数进行校验通过后,申请一段内存空间并写入数据,return_value返回该资源。PHP不需要知道资源的具体内部表示,只需要获取该资源存储的指针以及资源类型

函数接收资源参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PHP_FUNCTION(test_person_greet)
{
php_test_person *person;
zval *zperson;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_RESOURCE(zperson);
ZEND_PARSE_PARAMETERS_END();

person = (php_test_person *) zend_fetch_resource_ex(zperson, PHP_TEST_PERSON_RES_NAME, le_test_person);

php_printf("Hello ");
PHPWRITE(ZSTR_VAL(person->name), ZSTR_LEN(person->name));
php_printf("!\nAccording to my records, you are %ld years old.\n", person->age);

RETURN_TRUE;
}

ZEND_FETCH_RESOURCE在PHP7中已被删除,目前获取资源的函数是zend_fetch_resourcezend_fetch_resource_exzend_fetch_resource_ex函数需要一个zval、字面意义名称、资源类型,返回一个指针。函数内部切记不要free该指针

销毁资源

在PHP中使用fopen打开文件并获得一个资源句柄$fp,接下来unset($fp)时文件被关闭,即使没有使用fclose函数。其中的奥秘在zend_register_list_destructors_ex,该函数第一个参数为常规资源的析构函数,第二个为持久化资源的析构函数,当离开资源变量所在作用域时,自动调用清理/析构函数,释放内存、关闭连接或执行其他清理操作

1
2
3
4
5
6
7
8
9
10
11
12
13
le_test_person = zend_register_list_destructors_ex(php_test_person_dtor, NULL, PHP_TEST_PERSON_RES_NAME, module_number);

static void php_test_person_dtor(zend_resource *res)
{
php_test_person *person = (php_test_person *) res->ptr;

if (person) {
if (person->name) {
zend_string_release(person->name); // efree zend_string => zend_string_release
}
efree(person);
}
}

强制销毁资源

使用zend_list_delete销毁资源,该函数可销毁任何资源类型变量

1
2
3
4
5
6
7
8
9
10
11
12
PHP_FUNCTION(test_person_delete)
{
zval * zperson;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_RESOURCE(zperson);
ZEND_PARSE_PARAMETERS_END();

zend_list_delete(Z_RES_P(zperson));

RETURN_TRUE;
}

持久化资源

持久化资源与常规资源不同的地方在析构函数声明注册的位置,数据内存申请使用pemalloc代替emalloc

1
2
3
4
5
6
7
int le_test_person_persist;

PHP_MINIT_FUNCTION(test)
{
le_test_person = zend_register_list_destructors_ex(php_test_person_dtor, NULL, PHP_TEST_PERSON_RES_NAME, module_number);
le_test_person_persist = zend_register_list_destructors_ex(NULL, php_test_person_persist_dtor, PHP_TEST_PERSON_RES_NAME, module_number);
}

通常情况下,php_test_person_dtor会在请求结束后调用,php_test_person_persist_dtor在扩展shutdown阶段调用

1
2
3
4
5
6
7
8
9
10
11
static void php_test_person_persist_dtor(zend_resource *res)
{
php_test_person *person = (php_test_person *) res->ptr;

if (person) {
if (person->name) {
zend_string_release(person->name);
}
pefree(person, 1);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PHP_FUNCTION(test_person_pnew)
{
php_test_person * person;
zend_string * name;
zend_long age;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_STR(name);
Z_PARAM_LONG(age);
ZEND_PARSE_PARAMETERS_END();

if (age < 0 || age > 255) {
php_error_docref(NULL, E_WARNING, "Nonsense age (%ld) given, person resource not created.", age);
RETURN_FALSE;
}

person = pemalloc(sizeof(php_test_person), 1);
person->name = zend_string_dup(name, 1);
person->age = age;

RETURN_RES(zend_register_resource(person, le_test_person_persist));
}

test_person_pnewtest_person_new仅在数据初始化、资源类型方面有差异

查找已存在的持久化资源

为了可以重用持久化资源,需要将其保存在一个安全的地方,zend engine提供了一个executor global通过EG(persistent_list)访问,该变量类型为HashTable

重新修改test_person_pnew

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
PHP_FUNCTION(test_person_pnew2)
{
php_test_person * person;
char * key_raw;
size_t key_len;
zend_string * name, * key;
zend_long age;
zval * zperson = NULL;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_STR(name);
Z_PARAM_LONG(age);
ZEND_PARSE_PARAMETERS_END();

if (age < 0 || age > 255) {
php_error_docref(NULL, E_WARNING, "Nonsense age (%ld) given, person resource not created.", age);
RETURN_FALSE;
}

key_len = spprintf(&key_raw, 0, "test_person_%s_%ld\n", ZSTR_VAL(name), age);
key = zend_string_init(key_raw, key_len, 1);
efree(key_raw);

if ((zperson = zend_hash_find(&EG(persistent_list), key)) != NULL) {
person = (php_test_person *) zend_fetch_resource_ex(zperson, PHP_TEST_PERSON_RES_NAME, le_test_person_persist);

ZVAL_RES(return_value, zend_register_persistent_resource_ex(key, person, le_test_person_persist));
zend_string_release(key);
return ;
}

person = pemalloc(sizeof(php_test_person), 1);
person->name = zend_string_copy(name);
person->age = age;

ZVAL_RES(return_value, zend_register_persistent_resource_ex(key, person, le_test_person_persist));
zend_string_release(key);
}

test_person_pnew2先确定EG(persistent_list)是否已经存在,已存在则直接使用,不存在则申请内存初始化资源

参考文档

Extension Writing Part III: Resources
Extension Writing Part III: Resources
Upgrading PHP extensions from PHP5 to NG
PHP Internals Book
References about Maintaining and Extending PHP

PHP扩展开发第二章,主要介绍函数参数获取、zval数据类型、数组及数组遍历以及$GLOBALS全局变量访问

函数参数

扩展自定义函数不需要声明形参,Zend Engine会给每个函数传递一个参数列表zend_execute_data *execute_data,函数参数通过宏块ZEND_PARSE_PARAMETERS_STARTZEND_PARSE_PARAMETERS_END获取,当然,旧的方法zend_parse_parameters也可以使用,只是会更麻烦点

下面示例函数将接收参数name以及打印的次数t_param并打印name

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
PHP_FUNCTION(test)
{
// 参数1
zend_string *name;
// 参数2
zend_long t_param = 0;
int times = 1;

// 最低可接收1个参数,最多2个
ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STR(name)
Z_PARAM_OPTIONAL
Z_PARAM_LONG(t_param)
ZEND_PARSE_PARAMETERS_END();

// 判断是否有传递默认值
if (ZEND_NUM_ARGS() == 2) {
times = (int) (t_param < 1 ? 1 : t_param);
}

// 打印
for (int i = 0; i < times; i++) {
php_printf("Hello %s", ZSTR_VAL(name));
}
RETURN_TRUE;
}

常用的有数据类型以及参数获取方法有下

Variable Type Macro
zend_bool Z_PARAM_BOOL
zend_long Z_PARAM_LONG
double Z_PARAM_DOUBLE
zend_string * Z_PARAM_STR
char * Z_PARAM_STRING
zval * Z_PARAM_RESOURCE
zval * Z_PARAM_ARRAY
zval * Z_PARAM_OBJECT
zval * Z_PARAM_ZVAL

ZVAL结构

PHP7的zval相对PHP5做了比较大的改动,zval、zend_string、zend_array重构,bool类型分成了true、false两种类型直接存储在(zval).u1.type_info等

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
// php7 zval变量结构
typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;

struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t extra; /* not further specified */
} u)
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};

zval类型判断,一般情况下,如果参数与Z_PARAM_*的类型不一致,Zend Engine会进行转换

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
// 接收任意类型参数,最后输出其类型
PHP_FUNCTION(test_type)
{
zval *uservar;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ZVAL(uservar);
ZEND_PARSE_PARAMETERS_END();

switch (Z_TYPE_P(uservar)) {
case IS_NULL:
php_printf("NULL");
break;
case IS_TRUE:
php_printf("Boolean: TRUE");
break;
case IS_FALSE:
php_printf("Boolean: FALSE");
break;
case IS_LONG:
php_printf("Long: %ld", Z_LVAL_P(uservar));
break;
case IS_DOUBLE:
php_printf("Double: %f", Z_DVAL_P(uservar));
break;
case IS_STRING:
php_printf("String: ");
PHPWRITE(Z_STRVAL_P(uservar), Z_STRLEN_P (uservar));
break;
case IS_RESOURCE:
php_printf("Resource");
break;
case IS_ARRAY:
php_printf("Array");
break;
case IS_OBJECT:
php_printf("Object");
break;
default:
php_printf("Unknown");
}
}

打印数组内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PHP_FUNCTION(test_again)
{
zval *zname;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ZVAL(zname);
ZEND_PARSE_PARAMETERS_END();

// 将改变原有参数类型
convert_to_string(zname);
php_printf("Hello ");
PHPWRITE(Z_STRVAL_P(zname), Z_STRLEN_P(zname));
RETURN_TRUE;
}

convert_to_*会将参数转换成指定的类型,即改变原始参数类型、数据,在php代码中,原本以传参的形式传递的变量值会被修改。要解决这个问题,可以使用临时变量,也可以使用convert_to_*_ex,该类函数在转换类型前会先调用SEPARATE_ZVAL_IF_NOT_REF,避免修改原始变量

数组实现

PHP的数组用途非常广泛,类对象属性的存储也依赖数组,数组的底层实现结构为HashTable

下面函数创建一个数据并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PHP_FUNCTION(test_array)
{
// $arr = [];
array_init(return_value);
// $arr[3] = 123;
add_index_long(return_value, 3, 123);
// $arr[] = "example";
add_next_index_string(return_value, "example");

char *mystr = estrdup("five");
add_next_index_string(return_value, mystr);
efree(mystr);

// $arr["pi"] = 3.1415926;
add_assoc_double(return_value, "pi", 3.1415926);

zval subarr;
array_init(&subarr);
add_next_index_string(&subarr, "hello");
add_assoc_zval(return_value, "subarr", &subarr);
}
  • add_next_index_*添加元素进数组,由系统分配一个递增的数字key
  • add_index_*添加元素进数组,由用户指定一个数字类型的key
  • add_assoc_*添加元素进数组,由用户指定的字符串类型key

注意:add_assoc_*非二进制安全

扩展函数返回值不使用return语句,配置zval *return_value即可,该变量由宏PHP_FUNCTION提供,返回值RETURN_TRUE也是通过设置return_value完成返回值赋值,默认设置为IS_NULL类型

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
// 遍历并打印数组字符串值
PHP_FUNCTION(test_array_strings)
{
zval *arr, *data;
HashTable *arr_hash;
HashPosition pointer;
int array_count;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_ARRAY(arr);
ZEND_PARSE_PARAMETERS_END();

//
arr_hash = Z_ARRVAL_P(arr);
array_count = zend_hash_num_elements(arr_hash);

php_printf("The array passed contains %d elements\n", array_count);

/*
* 可使用宏ZEND_HASH_FOREACH_VAL、ZEND_HASH_FOREACH_END
*/
for (
zend_hash_internal_pointer_reset_ex(arr_hash, &pointer);
data = zend_hash_get_current_data_ex(arr_hash, &pointer);
zend_hash_move_forward_ex(arr_hash, &pointer)
) {
// example 1
// 如果是字符串就打印
// if (Z_TYPE_P(data) == IS_STRING) {
// PHPWRITE(Z_STRVAL_P(data), Z_STRLEN_P(data));
// php_printf("\n");
// }

// example 2
// 将参数转为字符串打印
// zval tmp;
// tmp = *data;
// zval_copy_ctor(&tmp);
// convert_to_string(&tmp);
// PHPWRITE(Z_STRVAL(tmp), Z_STRLEN(tmp));
// php_printf("\n");
// zval_dtor(&tmp);

// example 3
// 打印数组key以及value
int keytype;
zend_string * str_index;
zend_ulong num_index;

keytype = zend_hash_get_current_key_ex(arr_hash, &str_index, &num_index, &pointer);
if (HASH_KEY_IS_STRING == keytype) {
// 字符串类型key
PHPWRITE(ZSTR_VAL(str_index), ZSTR_LEN(str_index));
} else if (HASH_KEY_IS_LONG == keytype) {
// 数字key
php_printf("%ld", num_index);
}

php_printf(" => ");

zval tmp;
tmp = *data;
zval_copy_ctor(&tmp);
convert_to_string(&tmp);
PHPWRITE(Z_STRVAL(tmp), Z_STRLEN(tmp));
php_printf("\n");
zval_dtor(&tmp);
}

RETURN_TRUE;
}

zend_hash_get_current_key_ex返回的类型有

  • HASH_KEY_IS_LONG:整型key
  • HASH_KEY_IS_STRING:字符串型key
  • HASH_KEY_NON_EXISTENT:遍历完整个数组,没有更多元素
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
// 打印数组val
static int test_array_walk(zval *pDest)
{
zval tmp;

tmp = *pDest;
zval_copy_ctor(&tmp);
convert_to_string(&tmp);
PHPWRITE(Z_STRVAL(tmp), Z_STRLEN(tmp));
php_printf("\n");
zval_dtor(&tmp);

return ZEND_HASH_APPLY_KEEP;
}

// 打印prefix以及数组val
static int test_array_walk_arg(zval *pDest, void *argument)
{
php_printf("%s", (char *)argument);
test_array_walk(pDest);

return ZEND_HASH_APPLY_KEEP;
}

// 打印prefix、数组val以及suffix
static int test_array_walk_args(zval *pDest, int num_args, va_list args, zend_hash_key *hash_key)
{
php_printf("%s", va_arg(args, char *));
test_array_walk(pDest);
php_printf("%s\n", va_arg(args, char *));

return ZEND_HASH_APPLY_KEEP;
}

// 遍历数组
PHP_FUNCTION(test_array_walk)
{
zval *arr;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_ARRAY(arr);
ZEND_PARSE_PARAMETERS_END();

// 简单的遍历数组处理
zend_hash_apply(Z_ARRVAL_P(arr), (apply_func_t) test_array_walk);
// 遍历数组,带一个参数
zend_hash_apply_with_argument(Z_ARRVAL_P(arr), (apply_func_arg_t) test_array_walk_arg, "Hello");
// 遍历数组,带多个参数
zend_hash_apply_with_arguments(Z_ARRVAL_P(arr), (apply_func_args_t) test_array_walk_args, 2, "Hello ", "Welcome to my extension!");
}

上面test_array_walk类似array_maptest_array_walk简单遍历数组,test_array_walk_arg遍历数组,可接收一个任意类型的额外参数,下面代码中用作数组元素的前缀,test_array_walk_args遍历数组,可接收任意多个参数,下面代码中用作数组元素的前缀、后缀

返回值类型:

  • ZEND_HASH_APPLY_KEEP:维持原有元素,继续遍历数组剩余元素
  • ZEND_HASH_APPLY_REMOVE:删除原有元素,继续遍历数组剩余元素
  • ZEND_HASH_APPLY_STOP:维持原有元素,停止遍历数组
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
// 返回数组指定key的value
PHP_FUNCTION(test_array_value)
{
zval *arr, *offset, *val;
char * tmp;
zend_string *str_index = NULL;
zend_ulong num_index;

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_ARRAY(arr);
Z_PARAM_ZVAL(offset);
ZEND_PARSE_PARAMETERS_END();

// 对非integer/string的key进行转换
switch (Z_TYPE_P(offset)) {
case IS_NULL:
case IS_FALSE:
num_index = 0;
break;
case IS_TRUE:
num_index = 1;
break;
case IS_DOUBLE:
num_index = (long) Z_DVAL_P(offset);
break;
case IS_LONG:
case IS_RESOURCE:
num_index = Z_LVAL_P(offset);
break;
case IS_STRING:
str_index = zval_get_string(offset);
break;
case IS_ARRAY:
tmp = "Array";
str_index = zend_string_init(tmp, sizeof(tmp) - 1, 0);
break;
case IS_OBJECT:
tmp = "Object";
str_index = zend_string_init(tmp, sizeof(tmp) - 1, 0);
break;
default:
tmp = "Unknown";
str_index = zend_string_init(tmp, sizeof(tmp) - 1, 0);
}

if (str_index && (val = zend_hash_find(Z_ARRVAL_P(arr), str_index)) == NULL) {
RETURN_NULL();
} else if (!str_index && (val = zend_hash_index_find(Z_ARRVAL_P(arr), num_index)) == NULL) {
RETURN_NULL();
}

*return_value = *val;
zval_copy_ctor(return_value);
}

PHP7中zval在栈分配,函数执行完清理,需要深入复制一份给return_value

Symbol Tables

全局变量$GLOBALS的底层存储结构也是HashTable,存储在一个全局结构体Executor Globals(_zend_executor_globals)里,通过EG(symbol_table)访问相关数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 访问全局变量$GLOBALS
PHP_FUNCTION(test_get_global_var)
{
zval *val;
zend_string *varname;

ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_STR(varname);
ZEND_PARSE_PARAMETERS_END();

// 全局变量符号表:EG(symbol_table)
if ((val = zend_hash_find(&EG(symbol_table), varname)) == NULL) {
php_error_docref(NULL, E_NOTICE, "Undefined variable: %s", ZSTR_VAL(varname));
RETURN_NULL();
}

*return_value = *val;
zval_copy_ctor(return_value);
}

完整代码

php_ext_tutorial

参考文档

Extension Writing Part II: Parameters, Arrays, and ZVALs (Unable To Access)
Extension Writing Part II: Parameters, Arrays, and ZVALs [continued] (Unable To Access)
Extension Writing Part II: Parameters, Arrays, and ZVALs
Upgrading PHP extensions from PHP5 to NG
PHP Internals Book
References about Maintaining and Extending PHP
Internal value representation in PHP 7 - Part 1
Internal value representation in PHP 7 - Part 2
PHP’s new hashtable implementation

flamegraph是一款可视化工具,用户分析程序堆栈调用,x轴堆栈轨迹,y轴堆栈深度,x轴长度越长,占用更多cpu时间。下面简单介绍如何使用xdebug以及flamegraph生成火焰图

准备工作

配置xdebug参数

1
2
3
4
5
xdebug.trace_output_name = xdebug.trace.%t.%s
xdebug.trace_enable_trigger = 1
xdebug.trace_output_dir = /tmp
xdebug.trace_enable_trigger_value = "<secret key>"
xdebug.trace_format=1

下载flamegraph,git clone https://github.com/brendangregg/FlameGraph.git,放置在任意位置

flamegraph

获取堆栈数据

请求页面/接口url,在url后面增加?XDEBUG_TRACE=<secret key>,获取php堆栈数据 curl http://youdomain.com?XDEBUG_TRACE=<secret key>,如果是命令行命令,则在php命令后使用参数php -d xdebug.auto_trace=1

转换堆栈数据

1
./stackcollapse-xdebug.php /tmp/xdebug.trace.1579703170._data_www_yii2_web_index_php.xt > out.folded

生成火焰图

1
./flamegraph.pl out.folded > out.svg

示例

yii2 flamegraph example

参考

How to generate PHP Flamegraphs
Flame Graphs visualize profiled code

很长的一段时间,被xubuntu下的一个monospace字体显示问题困扰着,在网上找了很多方法也没有解决问题,在一次误打误撞的情况下解决了,简单把自己问题的过程记录下来,希望给一些小伙伴们帮助

问题描述

xubuntu下monospace在浏览器以及vscode下显示异常,具体如下图所示

浏览器chrome/firefox下的monospace

monofont display incorrect in browser

vscode下的monospace

monofont display incorrect in vscode

网上搜索结果大部分指示,是由于libfreetype6 2.8.1-2ubuntu2的一个bug导致,需要升级到最新的libfreetype2.9或者降级到libfreetype2.8.0,按照搜索的文档进行libfreetype的升级/降级后,显示效果依旧,问题没有得到解决

解决过程

在我放弃治疗后过了一段时间,突然想起,一直以来都忽略了验证linux的字体信息。首先,获取显示异常的字体font-family,如下图

browser get font family

使用fc-match验证字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ fc-match monospace
DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

$ fc-match consolas
DejaVuSansMono.ttf: "DejaVu Sans Mono" "Book"

$ fc-match Menlo
DejaVuSans.ttf: "DejaVu Sans" "Book"

$ fc-match "PingFang SC"
DejaVuSans.ttf: "DejaVu Sans" "Book"

$ fc-match "Microsoft YaHei"
NotoSansCJK-Regular.ttc: "Noto Sans CJK SC" "Regular"

可以看到Menlo、”PingFang SC”、”Microsoft YaHei”的实际字体跟monospace并不一致,抱着死马当活马医的心态,编辑字体配置文件~/.config/fontconfig/fonts.conf,内容如下,

1
2
3
4
5
6
7
8
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
<alias>
<family>Menlo</family>
<prefer><family>DejaVu Sans Mono</family></prefer>
</alias>
</fontconfig>

退出,重新登录,发现这个困扰了很久的问题终于解决,如下图

monofont the correct view

vscode可以通过更改font-family恢复正常的显示效果

注意

ubuntu/xubuntu、中文/英文的系统默认的字体会有些许差异,以上的操作可能不一定生效,可以新增~/.config/fontconfig/conf.d/00-menlo.conf

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias binding="same">
<family>Menlo</family>
<accept>
<family>DejaVu Sans Mono</family>
</accept>
</alias>
</fontconfig>

参考文档

[Ubuntu 18.04 LTS] Font not rendering properly
Ubuntu 18.04 LTS Font not rendering properly
Why doesn’t fc-match respect my match and edit rule for Courier when it does for Consolas?

工欲善其事必先利其器,xdebug是php开发的一个杀手级利器,vagrant是一款非常便捷的虚拟机管理工具,两者搭配有时会出现一些奇怪的问题,近期在重新配置开发环境时,出现xdebug连不上的异常情况,花了些时间才找到问题并解决,记录下解决问题的过程,希望能帮助到一些小伙伴

问题及解决过程

异常情况

xdebug配置后,客户端多次请求,vscode均无任何反应。检查服务端、客户端配置,确认没有差错;监测网络数据,只有初次请求数据,无结果返回

客户端/服务器配置信息

服务器是vagrant + ubuntu 18.04 + PHP 7.4,客户端是Windows 10,编辑器是vscode

初始xdebug配置如下

1
2
3
4
5
6
7
8
[xdebug]
xdebug.remote_enable = 1
xdebug.remote_autostart = 1
xdebug.remote_port = 9001
xdebug.remote_handler = "dbgp"
xdebug.remote_mode = req
xdebug.remote_connect_back = 1
xdebug.overload_var_dump=0

vscode配置launch.json如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9001,
"pathMappings": {
"/data/example": "D:\\data\\example"
},
"log": true
}
]
}

开启xdebug日志监测

1
xdebug.remote_log=/tmp/xdebug.log

再次请求,获取日志数据如下

1
2
3
4
5
6
7
[140114918627520] Log opened at 2019-09-04 11:49:00
[140114918627520] I: Checking remote connect back address.
[140114918627520] I: Checking header 'HTTP_X_FORWARDED_FOR'.
[140114918627520] I: Checking header 'REMOTE_ADDR'.
[140114918627520] I: Remote address found, connecting to 192.168.33.1:9001.
[140114918627520] E: Time-out connecting to client (Waited: 200 ms). :-(
[140114918627520] Log closed at 2019-09-04 11:49:00

日志显示服务端有正常向客户端9001端口发起连接,但客户端无响应导致超时,检查客户端防火墙无异常

最后,在谷歌上找到一个类似的问题,发现问题与vagrant有关,具体问题描述见参考文档链接

解决方法

1
2
3
; 移除 xdebug.remote_connect_back = 1
; 添加
xdebug.remote_host=10.0.2.2

重启php-fpm,再次测试,vscode可连接xdebug,问题解决

参考文档

How to configure Xdebug in PhpStorm through Vagrant
xdebug Time-out connecting to client

在网上找到几篇基于PHP5的扩展开发的文章,有些古老,与PHP7不完全兼容,自己做了些简单的翻译、修改。

建议开发者先阅读PHP Internals Book这份在线文档,里面对PHP做了比较全面的讲解。由于PHP扩展开发的资料比较少,也鲜有PHP开发者触及到这块,编写扩展时如果遇到问题,最好是先查找PHP源代码是否有相关的使用案例,再通过网上论坛、QQ等方式寻求帮助。

本文是PHP扩展开发系列的第一篇,主要介绍如何开发一个简单的PHP扩展、如何获取ini配置参数、如何配置全局变量

前期准备:

PHP编译选项加入--enable-debug以及--enable-maintainer-zts--enable-debug生成额外的调试符号,并将优化等级设置为-O0,报告内存泄露等错误,使使用gdb进行debug时更为准确;--enable-maintainer-zts启用线程安全,开启ZTS宏定义,配置PHP为TSRM机制,用于编写调试多线程代码

Hello World

扩展目录结构

你可能会使用PHP源代码自带的脚本ext/ext_skel.php生成扩展骨架,但这里选择极简方式创建一个扩展,最基本的扩展只需要三个文件:配置文件config.m4,头文件php_test.h,源文件test.c

test扩展目录结构

1
2
3
4
5
6
php-7.4.1
└── ext
└── test
├── config.m4
├── php_test.h
└── test.c

配置文件

1
2
3
4
5
6
7
8
9
10
dnl 用于生成configure及其他文件
dnl `dnl`开头的行为注释内容
dnl 若扩展依赖外部库,使用--with,否则默认使用--enable
PHP_ARG_ENABLE(test, whether to enable test support,
[ --enable-test Enable test support], no)

if test "$PHP_TEST" != "no"; then
AC_DEFINE(HAVE_TEST, 1, [ Have test support ])
PHP_NEW_EXTENSION(test, test.c, $ext_shared)
fi

头文件

头文件默认格式为php_extname.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef PHP_TEST_H
# define PHP_TEST_H

/* PHP扩展结构zend_module_entry */
extern zend_module_entry test_module_entry;
# define phpext_test_ptr &test_module_entry

/* version扩展版本号 */
# define PHP_TEST_VERSION "0.1.0"

/* 自定义函数声明PHP_FUNCTION(your_function_name) */
PHP_FUNCTION(test);

#if defined(ZTS) && defined(COMPILE_DL_TEST)
ZEND_TSRMLS_CACHE_EXTERN()
#endif

#endif

源文件

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
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "php_test.h"

/* 自定义函数注册,最终*test_functions注册到模块声明test_module_entry */
static const zend_function_entry test_functions[] = {
/*
* 注册`test`函数,格式为PHP_FE(func_name, arg_info),arg_info声明参数信息,如是否是引用等,这里先忽略
*/
PHP_FE(test, NULL)
PHP_FE_END
};

/* 模块注册 */
zend_module_entry test_module_entry = {
STANDARD_MODULE_HEADER,
"test", /* 扩展名称 */
test_functions, /* 自定义函数注册 */
NULL, /* PHP_MINIT */
NULL, /* PHP_MSHUTDOWN */
NULL, /* PHP_RINIT */
NULL, /* PHP_RSHUTDOWN */
NULL, /* PHP_MINFO */
PHP_TEST_VERSION, /* version */
STANDARD_MODULE_PROPERTIES
};

/* 函数test */
PHP_FUNCTION(test)
{
RETURN_STRING("Hello World");
}

#ifdef COMPILE_DL_TEST
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(test)
#endif

编译

1
2
3
4
phpize
./configure
# 除非config.m4有改动,否则只执行以下命令
make && sudo make install

验证

php.ini添加extension=test.so,执行脚本获取输出

1
php -r "echo test();" # 输出:Hello World

INI参数

Zend Engine提供了两种方式访问ini参数,以下是第一种较为简单的方式,第二种方式与全局变量搭配使用,后面介绍

我们在PHP的声明周期MINIT阶段注册ini参数,在php_test.h添加MINIT原型声明

1
2
3
4
// test模块初始化
PHP_MINIT_FUNCTION(test);
// test模块关闭清理函数
PHP_MSHUTDOWN_FUNCTION(test);

test.c做以下调整,注册MINIT,init参数声明

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
#include "php_ini.h"

zend_module_entry test_module_entry = {
STANDARD_MODULE_HEADER,
"test",
test_functions,
PHP_MINIT(test), /* 注册MINIT */
PHP_MSHUTDOWN(test), /* 注册MSHUTDOWN */
NULL,
NULL,
NULL,
PHP_TEST_VERSION,
STANDARD_MODULE_PROPERTIES
};

// ini参数获取代码块
PHP_INI_BEGIN()
// ini参数test.foo声明
PHP_INI_ENTRY("test.foo", "bar", PHP_INI_ALL, NULL)
PHP_INI_END();
// 以上宏等同于声明zend_ini_entry_def[]变量

PHP_MINIT_FUNCTION(test)
{
REGISTER_INI_ENTRIES();

return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(test)
{
UNREGISTER_INI_ENTRIES();

return SUCCESS;
}

PHP_FUNCTION(test)
{
RETURN_STRING(INI_STR("test.foo"));
}

ini参数声明放置在PHP_INI_BEGINPHP_INI_END,参数声明使用宏PHP_INI_ENTRY,具体格式为PHP_INI_ENTRY(param_name, default_val, access_mode_modifier, validator)

access_mode_modifier决定ini参数可在什么地方被修改,主要有

  • PHP_INI_ALL: 值可通过php.ini、.htaccess、ini_set修改
  • PHP_INI_SYSTEM: 只可通过php.ini修改
  • PHP_INI_PERDIR: 只可通过.htaccess修改

validator校验这里先忽略

ini参数值获取方式如下

当前值local value 默认值master value 返回值类型
INI_STR INI_ORIG_STR char *
INI_INT INI_ORIG_STR zend_long
INI_FLT INI_ORIG_STR double
INI_BOOL INI_ORIG_STR zend_bool

配置PHP,在php.ini添加test.foo=helloworld,执行脚本获取输出

1
php -r "echo test();" # 输出:helloworld

全局变量

上面部分的ini参数获取代码有几个问题,第一,每次读取ini参数值时,都需要系统扫描ini设置表,并转换成相应的类型;第二,没有校验用户输入,这样用户使用ini_set可以设置任意数据

我们可以配置ini参数到全局变量,避免每次扫描ini设置表,在这之前,先看看如何使用全局变量编写一个计数器

头文件php_test.h添加全局变量声明,并定义一个宏来访问我们的全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifdef ZTS
# include "TSRM.h"
#endif

/* 全局变量声明 */
ZEND_BEGIN_MODULE_GLOBALS(test)
long counter;
ZEND_END_MODULE_GLOBALS(test)
/*
* 上面代码等同
* typedef struct _zend_pib_globals {
* long counter;
* } zend_test_globals;
*/

/* 宏定义 TEST_G - 全局变量访问 */
#ifdef ZTS
# define TEST_G(v) TSRMG(test_globals_id, zend_test_globals *, v)
#else
# define TEST_G(v) (test_globals.v)
#endif

/* 请求初始化 */
PHP_MINIT_FUNCTION(test);

源文件初始化全局变量,MINIT只会在进程或线程创建时执行一次,而一个进程可以服务多个请求,我们需要计数器在每个请求开始时清零,需要RINIT函数清零counter

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
/* 初始化zend_test_globals */
ZEND_DECLARE_MODULE_GLOBALS(test)
/* 等同于 zend_test_globals test_globals; */

zend_module_entry test_module_entry = {
STANDARD_MODULE_HEADER,
"test",
test_functions,
PHP_MINIT(test),
PHP_MSHUTDOWN(test),
PHP_RINIT(test), /* 注册请求初始化函数 */
NULL,
NULL,
PHP_TEST_VERSION,
STANDARD_MODULE_PROPERTIES
};

/* 全局变量初始化,仅在进程/线程创建时初始化 */
static void php_test_init_globals(zend_test_globals *test_globals) {}

// 请求初始化
PHP_RINIT_FUNCTION(test)
{
/* 因一个进程可服务多个请求,我们需要每个请求都有独立的counter */
TEST_G(counter) = 0;

return SUCCESS;
}

// 模块初始化
PHP_MINIT_FUNCTION(test)
{
/* 全局变量表注册 */
ZEND_INIT_MODULE_GLOBALS(test, php_test_init_globals, NULL);

REGISTER_INI_ENTRIES();

return SUCCESS;
}

执行PHP脚本获取输出验证

1
php -r "test(); echo test();" # 输出:2

INI注册全局变量

有了上面的认识,在这里,将ini参数test.direction注册到全局变量,并添加validator校验

头文件添加参数声明

1
2
3
4
ZEND_BEGIN_MODULE_GLOBALS(test)
long counter;
zend_bool direction;
ZEND_END_MODULE_GLOBALS(test)

源文件修改为以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PHP_INI_BEGIN()
PHP_INI_ENTRY("test.foo", "bar", PHP_INI_ALL, NULL)
// STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr)
STD_PHP_INI_ENTRY("test.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_test_globals, test_globals)
PHP_INI_END();

static void php_test_init_globals(zend_test_globals *test_globals) {
/* direction初始化,0|1 */
test_globals->direction = 1;
}

PHP_FUNCTION(test)
{
if (TEST_G(direction)) {
TEST_G(counter)++;
} else {
TEST_G(counter)--;
}

RETURN_LONG(TEST_G(counter));
}

STD_PHP_INI_ENTRY可以使用更多参数,这里我们将参数写入全局变量test_globals。关联第四个参数on_modify校验输入值,如果成功,写入到全局变量,默认为onUpdateLongGEZero,其他的宏还有OnUpdateLongOnUpdateBoolOnUpdateRealOnUpdateStringOnUpdateStringUnempty

配置PHP,在php.ini添加test.direction=0,执行脚本获取输出

1
php -r "echo test();" # 输出:-1

完整代码

php_ext_tutorial

参考文档

How to make a PHP extension
Upgrading PHP extensions from PHP5 to NG
Extension Writing Part I: Introduction to PHP and Zend (已失效)
Extension Writing Part I: Introduction to PHP and Zend
PHP Internals Book
References about Maintaining and Extending PHP

简单介绍如何使用github pages服务以及hexo搭建一个免费的个人博客

准备

创建仓库

创建一个空的仓库,名字固定格式为“username.github.io”,这个名字也是博客的访问的域名

安装hexo

1
2
3
4
5
6
7
npm install -g hexo-cli           # 安装hexo命令行工具
hexo init blog # 创建blog项目

cd blog
git clone https://github.com/theme-next/hexo-theme-next themes/next # 安装next主题,可选

npm install hexo-deployer-git --save # 安装github发布工具

配置

配置hexo

修改_config.yml

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
# 基础配置
# Hexo Configuration
## Docs: https://hexo.io/docs/configuration.html
## Source: https://github.com/hexojs/hexo/

# Site
title: Y2k38's Blog
subtitle:
description: y2k38's blog
keywords:
author: y2k38
language: zh-CN
timezone:

# URL
## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
url: https://y2k38.github.io
root: /
permalink: :year/:month/:day/:title/
permalink_defaults:

...

# 博客主题配置
# Extensions
## Plugins: https://hexo.io/plugins/
## Themes: https://hexo.io/themes/
theme: next # 可选,若没有安装next主题,则忽略

...

# github部署配置
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
type: git
repo: https://github.com/Y2k38/y2k38.github.io.git
branch: master # 注意,分支固定位master

发布github

1
hexo clean && hexo deploy

完成

其他

添加gitalk评论系统

[Deprecated] github要求校验方式改为personal access token,待gitalk作者及theme-next作者更新

新建OAuth application获取id、secret,https://github.com/settings/applications/new

new oauth app

配置next主题 themes/next/_config.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Gitalk
# Demo: https://gitalk.github.io
# For more information: https://github.com/gitalk/gitalk, https://github.com/emn178/js-md5
gitalk:
enable: true
github_id: y2k38 # GitHub repo owner
repo: y2k38.github.io # Repository name to store issues
client_id: xxxxxxxxxxxxxxxx # GitHub Application Client ID
client_secret: yyyyyyyyyyyyyyyy # GitHub Application Client Secret
admin_user: Y2k38 # GitHub repo owner and collaborators, only these guys can initialize gitHub issues
distraction_free_mode: true # Facebook-like distraction free mode
# Gitalk's display language depends on user's browser or system environment
# If you want everyone visiting your site to see a uniform language, you can set a force language value
# Available values: en | es-ES | fr | ru | zh-CN | zh-TW
language: zh-CN

参考文档

GitHub Pages
How to use Hexo and deploy to GitHub Pages
Creating a personal access token for the command line

0%