在网上找到几篇基于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

简单介绍如何生成https服务所需要的证书、私钥以及部署,文档分别描述了开发时使用的self-signed证书以及线上使用的let’s encrypt免费证书的生成方法

自签名证书

创建证书以及私钥

1
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt
  • req: 指定X.509证书签名请求(CSR)管理

  • -x509: 生成self-sigend证书

  • -nodes: 不使用密码

  • -days 365: 证书有效期

  • -newkey rsa:2048: 生成证书的同时生成私钥,rsa加密算法,2048位

  • -keyout: 私钥的存储路径

  • -out: 证书的存储路径

信息填写

1
2
3
4
5
6
7
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:New York
Locality Name (eg, city) []:New York City
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Bouncy Castles, Inc.
Organizational Unit Name (eg, section) []:Ministry of Water Slides
Common Name (e.g. server FQDN or YOUR name) []: example.com (or server_IP_address)
Email Address []:admin@your_domain.com

配置nginx

1
sudo vi /etc/nginx/sites-enabled/example.com

添加证书以及私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 443 ssl;
listen [::]:443 ssl;

ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;

server_name example.com;

root /var/www/example.com/html;
index index.html index.htm index.nginx-debian.html;

. . .
}

重启nginx,完成

正式证书

let’t encrypt安装

1
sudo git clone https://github.com/letsencrypt/letsencrypt /usr/local/letsencrypt

生成证书以及私钥

生成指定域名证书及密钥

1
sudo -H /data/app/letsencrypt/letsencrypt-auto certonly --standalone -d www.example.com

letsencrypt需要记录服务器ip,选择“Y”。打开新的命令行窗口,在指定的目录.well-known下创建文件并将指定的内容写入后,在原命令行窗口点击enter键结束

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
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for www.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Create a file containing just this data:

BxYgbmNBP0glztPgaevZwzwMSPy2Asd8LRzlyQNnax9.sAK0-w3-GGiCDaAreDXX1rxwm0qQfBfhb-4765BnzRZ

And make it available on your web server at this URL:

http://www.example.com/.well-known/acme-challenge/BxYgbmNBP0glztPgaevZwzwMSPy2Asd8LRzlyQNnax9

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue # 文件创建后再确认
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/www.example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/www.example.com/privkey.pem
Your cert will expire on 2019-11-04. To obtain a new or tweaked
version of this certificate in the future, simply run
letsencrypt-auto again. To non-interactively renew *all* of your
certificates, run "letsencrypt-auto renew"
- If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

生成wildcard domain证书及密钥

1
2
# 如果嫌单个域名太麻烦,想配置wildcard证书,使用下面的命令
sudo -H /data/app/letsencrypt/letsencrypt-auto certonly --manual --preferred-challenges=dns -d *.example.com

按提示添加dns记录后再点击enter结束

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
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for example.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.example.com with the following value:

AhAGaqohVH8JMAS-jHkS5L_i2J4JCnglUJvY5n2S9qI

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue # 需添加dns记录
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/example.com/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/example.com/privkey.pem
Your cert will expire on 2019-11-04. To obtain a new or tweaked
version of this certificate in the future, simply run
letsencrypt-auto again. To non-interactively renew *all* of your
certificates, run "letsencrypt-auto renew"
- If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

配置nginx

1
sudo vi /etc/nginx/sites-enabled/example.com

添加证书以及私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 443 ssl;
listen [::]:443 ssl;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

server_name example.com;

root /var/www/example.com/html;
index index.html index.htm index.nginx-debian.html;

. . .
}

重启nginx,完成

参考文档

How To Create a Self-Signed SSL Certificate for Nginx in Ubuntu 18.04
Install Let’s Encrypt to Create SSL Certificates
How to setup Let’s Encrypt for Nginx on Ubuntu 18.04 (including IPv6, HTTP/2 and A+ SLL rating)
Certbot User Guide
letsencrypt man

0%