本篇主要讲例子,至于关于FFI的知识点,可以参考以下文档:

官网文档FFI介绍入口 http://php.p2hp.com/manual/zh/class.ffi.php

或 https://www.kancloud.cn/a173512/php_note/1690512

鸟哥关于FFI的文章 https://www.laruence.com/2020/03/11/5475.html

cpp命令 https://blog.csdn.net/leibris/article/details/120195897

cpp命令 https://blog.csdn.net/he3913/article/details/2031050

首先检查 ffi 扩展是否已经安装 (配置文件是否存在,动态库是否存在)。ffi包含在 php8.0-common 软件包中,因此一般都是安装了的,毕竟它是语言基本特性。

sjg@sjg-PC:~/garbage$ php -r "phpinfo();" | grep ffi
/etc/php/8.0/cli/conf.d/20-ffi.ini,
ffi.enable => preload => preload
ffi.preload => no value => no value
sjg@sjg-PC:~/garbage$ php -r "phpinfo();" | grep extension_dir
extension_dir => /usr/lib/php/20200930 => /usr/lib/php/20200930
sqlite3.extension_dir => no value => no value
sjg@sjg-PC:~/garbage$ ll /usr/lib/php/20200930/ | grep ffi
-rw-r--r-- 1 root root  165992 2月  15 02:29 ffi.so

我们来写一个动态库,它实现加盐的 SHA1算法(其实就是把RFC3174抄写一遍稍加改写),如果没有指定盐,就使用默认盐zime87772676。打开Clion,创建项目类型为 C Library,然后头文件library.h 和实现文件 library.c 如下:

#ifndef MYSHA1SO_LIBRARY_H
#define MYSHA1SO_LIBRARY_H#include <stdint-gcc.h>enum {shaSuccess = 0,shaNull,            /* Null pointer parameter */shaInputTooLong,    /* input data too long */shaStateError       /* called Input after Result */
};
#define SHA1HashSize 20   // sha1 结果为 20字节 长度,常表示成 40个 16进制数字/** 消息长度指的是 bit数, bit数 是8的倍数就可以用 16进制数字 表达* SHA1要求消息长度为512的倍数,即 512*n bit,不是512的倍数就需要后续“补白操作”* 补白操作步骤:设原始消息长度为l,则在后面先补白1个bit “1”,然后是一堆 “0”,再* 补上64bit来表示原始消息长度l,具体中间补多少个 “0”,是要保证最后补上的64bit(8字节)* 凑齐总共 512*n bit 或者说 16*n 个 word(32-bit),亦或 64*n 个字节* 原始消息长度l存放时是Big-Endian的(左边字节是高位)*/
typedef struct SHA1Context {uint32_t Intermediate_Hash[SHA1HashSize / 4]; /* Message Digest 中间摘要或最终摘要  */uint32_t Length_Low;            /* Message length in bits      */uint32_t Length_High;           /* Message length in bits      *//* Index into message block array   block缓冲区 为 512-bit,即 64 字节   */int_least16_t Message_Block_Index;uint8_t Message_Block[64];      /* 512-bit message blocks */int Computed;               /* Is the digest computed?         */int Corrupted;             /* Is the message digest corrupted? */
} SHA1Context;int SHA1Reset(SHA1Context *);int SHA1Input(SHA1Context *, const uint8_t *, unsigned int);int SHA1Result(SHA1Context *, uint8_t Message_Digest[SHA1HashSize]);void SHA1ProcessMessageBlock(SHA1Context *context);void SHA1PadMessage(SHA1Context *context);uint32_t mySHA1(const uint8_t *in, uint8_t *out, const char *salt);#endif
#include "library.h"#include <stdio.h>
#include <string.h>// 参考 https://www.ietf.org/rfc/rfc3174.txt// 一个 word 为 32-bit,可以表示 0 到 2^32-1 的整数,以下操作为 32-bit word的循环左移操作
#define SHA1CircularShift(bits, word) \(((word) << (bits)) | ((word) >> (32-(bits))))//void SHA1PadMessage(SHA1Context *);
//
//void SHA1ProcessMessageBlock(SHA1Context *);int SHA1Reset(SHA1Context *context) {               // 初始化 contextif (!context) {return shaNull;}context->Length_Low = 0;context->Length_High = 0;context->Message_Block_Index = 0;context->Intermediate_Hash[0] = 0x67452301;     // H0context->Intermediate_Hash[1] = 0xEFCDAB89;     // H1context->Intermediate_Hash[2] = 0x98BADCFE;     // H2context->Intermediate_Hash[3] = 0x10325476;     // H3context->Intermediate_Hash[4] = 0xC3D2E1F0;     // H4context->Computed = 0;context->Corrupted = 0;return shaSuccess;
}int SHA1Result(SHA1Context *context, uint8_t Message_Digest[SHA1HashSize]) {int i;if (!context || !Message_Digest) {return shaNull;}if (context->Corrupted) {return context->Corrupted;}if (!context->Computed) {     // 未计算过SHA1PadMessage(context);  // 可能最终长度不满足要求,补白(包含了摘要计算)for (i = 0; i < 64; ++i) {/* message may be sensitive, clear it out 计算完清除原始消息块 */context->Message_Block[i] = 0;}context->Length_Low = 0;    /* and clear length */context->Length_High = 0;context->Computed = 1;    // 标记“已经计算”}// H0 >> 24, H0 >> 16, H0 >> 8, H0 >> 0, H1 >> 24, H1 >> 16, H1 >> 8 ...for (i = 0; i < SHA1HashSize; ++i) {Message_Digest[i] = context->Intermediate_Hash[i >> 2]>> 8 * (3 - (i & 0x03));}return shaSuccess;
}int SHA1Input(SHA1Context *context, const uint8_t *message_array, unsigned length) {if (!length) {return shaSuccess;}if (!context || !message_array) {return shaNull;}if (context->Computed) {context->Corrupted = shaStateError;return shaStateError;}if (context->Corrupted) {return context->Corrupted;}while (length-- && !context->Corrupted) {context->Message_Block[context->Message_Block_Index++] =(*message_array & 0xFF);context->Length_Low += 8;if (context->Length_Low == 0) {context->Length_High++;if (context->Length_High == 0) {/* Message is too long */context->Corrupted = 1;}}if (context->Message_Block_Index == 64) {  // 缓冲区满,先计算摘要SHA1ProcessMessageBlock(context);}message_array++;  // 下一字节}return shaSuccess;
}void SHA1ProcessMessageBlock(SHA1Context *context) {const uint32_t K[] = {       /* Constants defined in SHA-1, K(t)=K[t/20] */0x5A827999,     // for t in [0, 19]0x6ED9EBA1,     // for t in [20, 39]0x8F1BBCDC,     // for t in [40, 59]0xCA62C1D6      // for t in [60, 79]};int t;                 /* Loop counter                */uint32_t temp;              /* Temporary word value        */uint32_t W[80];             /* Word sequence               */uint32_t A, B, C, D, E;     /* Word buffers                *//**  Initialize the first 16 words in the array W*/for (t = 0; t < 16; t++) {    // W[0]...W[15] 相当于从块缓冲读入 16个 32-bit 整数(左边高位)W[t] = context->Message_Block[t * 4] << 24;W[t] |= context->Message_Block[t * 4 + 1] << 16;W[t] |= context->Message_Block[t * 4 + 2] << 8;W[t] |= context->Message_Block[t * 4 + 3];}for (t = 16; t < 80; t++) {  // W[16]...W[79] 是 0x1 循环左移 x位 得到,x依赖前面16个整数W[t] = SHA1CircularShift(1, W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16]);}A = context->Intermediate_Hash[0];B = context->Intermediate_Hash[1];C = context->Intermediate_Hash[2];D = context->Intermediate_Hash[3];E = context->Intermediate_Hash[4];for (t = 0; t < 20; t++) {temp = SHA1CircularShift(5, A) +((B & C) | ((~B) & D)) + E + W[t] + K[0];   // f(t;B,C,D), where t in [0,19]E = D;D = C;C = SHA1CircularShift(30, B);B = A;A = temp;}for (t = 20; t < 40; t++) {temp = SHA1CircularShift(5, A) +(B ^ C ^ D) + E + W[t] + K[1];          // f(t;B,C,D), where t in [20,39]E = D;D = C;C = SHA1CircularShift(30, B);B = A;A = temp;}for (t = 40; t < 60; t++) {temp = SHA1CircularShift(5, A) +((B & C) | (B & D) | (C & D)) + E + W[t] + K[2];// f(t;B,C,D), where t in [40,59]E = D;D = C;C = SHA1CircularShift(30, B);B = A;A = temp;}for (t = 60; t < 80; t++) {temp = SHA1CircularShift(5, A) +(B ^ C ^ D) + E + W[t] + K[3];              // f(t;B,C,D), where t in [60,79]E = D;D = C;C = SHA1CircularShift(30, B);B = A;A = temp;}context->Intermediate_Hash[0] += A;     // H0 ~ H4 是计算后的消息摘要context->Intermediate_Hash[1] += B;context->Intermediate_Hash[2] += C;context->Intermediate_Hash[3] += D;context->Intermediate_Hash[4] += E;context->Message_Block_Index = 0;       // 摘要计算完总是重置消息块索引为 0
}void SHA1PadMessage(SHA1Context *context) {     // 补白中包含了摘要计算!!/**  Check to see if the current message block is too small to hold*  the initial padding bits and length.  If so, we will pad the*  block, process it, and then continue padding into a second*  block.*/if (context->Message_Block_Index > 55) {  // 块缓冲比较满,不能常规补白,先计算部分摘要context->Message_Block[context->Message_Block_Index++] = 0x80;while (context->Message_Block_Index < 64) {context->Message_Block[context->Message_Block_Index++] = 0;}SHA1ProcessMessageBlock(context);while (context->Message_Block_Index < 56) {context->Message_Block[context->Message_Block_Index++] = 0;}} else {context->Message_Block[context->Message_Block_Index++] = 0x80;while (context->Message_Block_Index < 56) {context->Message_Block[context->Message_Block_Index++] = 0;}}/**  Store the message length as the last 8 octets*/context->Message_Block[56] = context->Length_High >> 24;context->Message_Block[57] = context->Length_High >> 16;context->Message_Block[58] = context->Length_High >> 8;context->Message_Block[59] = context->Length_High;context->Message_Block[60] = context->Length_Low >> 24;context->Message_Block[61] = context->Length_Low >> 16;context->Message_Block[62] = context->Length_Low >> 8;context->Message_Block[63] = context->Length_Low;SHA1ProcessMessageBlock(context);
}const char defaultSalt[] = "zime87772676";uint32_t mySHA1(const uint8_t *in, uint8_t *out, const char *salt) {char mySalt[64] = {0};uint8_t buf[4096];unsigned int len = 0;strcpy(mySalt, *salt != '\0' ? salt : defaultSalt);  // 使用指定的salt或默认salt (传入空串用默认串)uint8_t *p = buf, *q = (uint8_t *)in;while (*q) {        // 将 in[] 装入 buf[]*p = *q;++p, ++q, ++len;}q = (uint8_t *)mySalt;while (*q)  {       // 将 mySalt[] 装入 buf[]*p = *q;++p, ++q, ++len;}SHA1Context context;uint32_t myLastError = SHA1Reset(&context);  // 初始化if (myLastError) return myLastError;myLastError = SHA1Input(&context, buf, len);  // 从buf输入,包含了摘要计算if (myLastError) return myLastError + 100;myLastError = SHA1Result(&context, out);  // 输出结果到out,包含了可能的补白和摘要计算if (myLastError) return myLastError + 200;return myLastError;
}

把项目 build 生成 libmySHA1so.so (会自动在项目名前加lib,然后后缀.so)。当然,直接终端下用gcc生成.so也不会太麻烦(gcc -shared -fPIC library.c -o libmySHA1so.so),不是我们的重点,忽略。

我们先写一个C程序来大概测试一下 (library.h头文件和.so动态库拷贝到和以下文件同一目录下)

#include "library.h"
#include <stdio.h>int main()
{uint8_t in[1024] = "sjg";uint8_t out[200];int i;const char salt[] = "beta";if (0 == mySHA1(in, out, salt)) {for (i = 0; i < 20; i++) {printf("%02x", out[i]);}printf("\n");} else {printf("error cacl %s%s\n", in, salt);}if (0 == mySHA1(in, out, "")) {for (i = 0; i < 20; i++) {printf("%02x", out[i]);}printf("\n");} else {printf("error cacl %s%s\n", in, "");}return 0;
}
sjg@sjg-PC:~/garbage$ gcc test1.c -L. -lmySHA1so -o test1
sjg@sjg-PC:~/garbage$ ./test1
./test1: error while loading shared libraries: libmySHA1so.so: cannot open shared object file: No such file or directory

注意:-L编译时库搜索路径 -l库名 (库名不包含前缀lib和后缀.so)。运行时,提示找不到动态库,我们要把.so文件所在路径添加到环境变量LD_LIBRARY_PATH中(动态库路径细节可以参考 https://www.bbsmax.com/A/ZOJP4lZeJv/)

sjg@sjg-PC:~/garbage$ export  LD_LIBRARY_PATH=/home/sjg/garbage:$LD_LIBRARY_PATH
sjg@sjg-PC:~/garbage$ ./test1
1662f332c84db3fc0590bf39eca2a3912c9bfc27
94576415f34481c90986912998f0e3e5ba79cc4e

我们写一个php来看如何动态加载这个so里面的函数并使用。主要步骤包含创建FFI对象(相当于访问代理),创建一些C数据结构用于参数的传入传出,用FFI对象调用有关函数。下面的例子中,FFI对象“包裹”了.so文件中的 mySHA1 函数,$a 是传入参数FFI\CData对象,$b和$c是传出参数FFI\CData对象。用mySHA1分别计算 密码sjg + 用户设定盐beta 和 密码sjg + 无用户设定盐(使用默认盐zime87772676),同时和PHP自身的sha1函数结果对比。

<?php$libPath = __DIR__.'/libmySHA1so.so';  // .so库文件路径
$ffi = \FFI::cdef(<<<EOF
uint32_t mySHA1(const uint8_t *in, uint8_t *out, const char *salt);
EOF, $libPath);$passwordText = 'sjg';
$salt = 'beta';
$a = FFI::new("uint8_t[1024]");
$b = FFI::new("uint8_t[100]");
$c = FFI::new("uint8_t[100]");
for ($i = 0; $i < strlen($passwordText); $i++)$a[$i] = ord($passwordText[$i]);$error = $ffi->mySHA1($a, $b, $salt);
echo $error.PHP_EOL;$hash = '';
for ($i = 0; $i < 20; $i++)$hash .= $b[$i] > 0xf ? dechex($b[$i]) : '0'.dechex($b[$i]);
echo $hash.PHP_EOL;
echo sha1($passwordText . $salt).PHP_EOL;$error = $ffi->mySHA1($a, $c, "");
echo $error.PHP_EOL;
$hash = '';
for ($i = 0; $i < 20; $i++)$hash .= $c[$i] > 0xf ? dechex($c[$i]) : '0'.dechex($c[$i]);
echo $hash.PHP_EOL;
$salt = 'zime87772676';
echo sha1($passwordText.$salt).PHP_EOL;

运行结果如下:

sjg@sjg-PC:~/garbage$ php testFFI.php
0
1662f332c84db3fc0590bf39eca2a3912c9bfc27
1662f332c84db3fc0590bf39eca2a3912c9bfc27
0
94576415f34481c90986912998f0e3e5ba79cc4e
94576415f34481c90986912998f0e3e5ba79cc4e

在已经有.h头文件的情况下,自己手工去“包裹”需要用到的函数后结构,不够方便,而且当函数比较多的时候更是如此(包裹的内容并非都可以从.h拷贝就可以完事的)。我们可以利用已有的.h头文件,生成一个“中介”.h头文件,然后利用该“中介”头文件来加载创建FFI对象。

先创建“中介”头文件mySHA1.h,里面添加以下内容 (FFI::load()用于从C头文件加载声明,宏定义FFI_LIB指明哪个.so应当被加载,尽管似乎没有什么用,但似乎此种方式要求宏定义FFI_SCOPE存在)

#define FFI_SCOPE "MYSHA1"
#define FFI_LIB  "/home/sjg/garbage/libmySHA1so.so"

执行命令 cpp -P library.h >> mySHA1.h 将library.h头文件进行文件包含和宏替换预处理后,将预处理结果追加到mySHA1.h中,这样可以避免手动添加类型定义和函数声明。

最后,使用动态库中函数的例子开头部分改写成如下(后面部分代码一样)。(说明:这里的做法类似PHP8源码中测试文件 ext/ffi/tests/301.phpt 这个例子)

<?php$ffi = FFI::load(__DIR__.'/mySHA1.h');$passwordText = 'sjg';
$salt = 'beta';

无论是官网关于FFI的说明还是鸟哥文章,都提到FFI的性能问题:每次请求都加载动态库并使用里面的函数和数据结构是开销很高的,减轻影响的办法是使用“预加载(preload)”。

“预加载(preload)”有2种做法:一种是预加载“中介”.h头文件;另一种是使用额外的“中介”预加载php文件,用此“中介”php加载“中介”.h头文件,同时将此php缓存到opcache。

先来看第一种。修改php配置文件php.ini (可以php -r "phpinfo();" | more 查看Loaded Configuration File)差不多最后部分(事实上,我是看配置中注释部分含义来推断应该直接写.h头文件的):

[ffi]
; FFI API restriction. Possible values:
; "preload" - enabled in CLI scripts and preloaded files (default)
; "false"   - always disabled
; "true"    - always enabled
ffi.enable=true; List of headers files to preload, wildcard patterns allowed.
ffi.preload="/home/sjg/garbage/mySHA1.h"

使用的部分只需要把创建FFI对象实例的部分改一下就行(用FFI::scope()创建),其余不变:

<?php$ffi = FFI::scope('MYSHA1');$passwordText = 'sjg';
$salt = 'beta';

再来看第二种。参照PHP8源码中测试文件 ext/ffi/tests/300.phpt,修改php配置文件php.ini,在[opcache]小节,设置如下 (在 opcache 预加载 ffi_preload.php文件)

[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.preload="/home/sjg/garbage/ffi_preload.php"
opcache.file_cache_only=0

而[ffi]小节只启用ffi,修改设置如下 (这里启用ffi为preload,即默认值,此时,对于CLI模式,ffi总是启用的,而非CLI模式,就是只有opcache预加载的文件才能使用ffi了,这就是鸟哥文章中说的考虑“安全”的情况了)

[ffi]
; FFI API restriction. Possible values:
; "preload" - enabled in CLI scripts and preloaded files (default)
; "false"   - always disabled
; "true"    - always enabled
ffi.enable=preload; List of headers files to preload, wildcard patterns allowed.
;ffi.preload="/home/sjg/garbage/mySHA1.h"

ffi_preload.php文件比较简单,就是用FFI::load()加载“中介”.h文件(mySHA1.h同前)

<?php$ffi = FFI::load(__DIR__.'/mySHA1.h');

使用的方式和前面是一样的(故代码就是前面的代码),即 $ffi = FFI::scope('MYSHA1'); 创建 FFI实例对象。

鸟哥的文章中“安全”的做法,指的是预加载的php文件中,把需要的功能打包好,然后避免FFI有关功能被其他PHP用到(除了调用我们打包的函数以外)

PHP 7.4/8.x FFI的使用例子相关推荐

  1. 开源游戏机java模拟器_开源一个Flutter编写的完整终端模拟器

    上次开源了一个简易的终端模拟器,我也知道并不是标准的,但自己也一直在用,然后就发现了一些棘手的问题,就又跑去研究了一些完整终端的源码,termux,Android Terminal,最后成功的将他们的 ...

  2. torch.utils.ffi is deprecated. Please use cpp extensions instead

    torch.utils.ffi is deprecated. Please use cpp extensions instead torch1.6 第一步 在build.py文件中的找到语句: fro ...

  3. Electron通过ffi调用DLL

    第一步建立一个DLL int WINAPI CAM_Open(char *pIn, char* pOut); 第二步安装ffi npm install --save ffi 针对electron版本重 ...

  4. NodeJS通过ffi调用DLL

    第一步建立一个dll, 提供方法如下 int WINAPI CAM_Open(char *pIn, char* pOut); 第二步安装ffi (前提已安装python2.x环境) npm insta ...

  5. luajit日记-FFI库

    2019独角兽企业重金招聘Python工程师标准>>> LuaJIT FFI LibraryThe FFI library allows calling external C fun ...

  6. php curl https_PHP FFI:一种全新的PHP扩展方式

    (给PHP开发者加星标,提升PHP技能) 转自:laruence/鸟哥 www.laruence.com/2020/03/11/5475.html 随着 PHP7.4 而来的有一个我认为非常有用的一个 ...

  7. 函数头文件php_PHP 7.4 的 FFI 将支持更好地在 PHP 中使用 C 函数/数据结构

    即将在下月底发布的 PHP 7.4 将会引入一个有趣的新功能,那就是期待已久的对 FFI(外部函数接口,Foreign Function Interface)的支持.此功能使得开发者可以在原生 PHP ...

  8. vue 调用c++_electron-vue跨平台桌面应用开发实战教程(七)——ffi调用C++(macOS平台)...

    electron功能很强大,但是有一些跟操作系统底层交互的功能,electron无法实现,这个时候我们就可以调用原生来配合完成对应功能,本文主要讲解在macOS平台下,调用C++的dylib文件 在开 ...

  9. Rust FFI 编程--理解不同语言的数据类型转换

    1. 简介 "FFI"是" Foreign Function Interface"的缩写,大意为不同编程语言所写程序间的相互调用.鉴于C语言事实上是编程语言界的 ...

最新文章

  1. 咱们从头到尾说一次 Java 垃圾回收
  2. Spring+Hibernate的典型配置
  3. UI 设计:如何做到理性?
  4. 零基础学习Python容易出现哪些误区?
  5. 刘强东发新年信:过去一年我们异常艰难
  6. DefaultKeyedVector和KeyedVector用法
  7. sql 在某段时间_解Bug之路记一次中间件导致的慢SQL排查过程
  8. eclipse配置xml的自动提示
  9. 3.2. tensorflow2实现Wileoxon秩和检验法(下) ——python实战
  10. halcon例程讲解_halcon例程学习笔记(6)
  11. 通过 Bitmap Font Generator 生成 fnt 与 png 文件供 cocos2d-x 中 LabelBMFont 使用达到以图片表现数字
  12. 开源OA协同办公平台使用教程:O2OA集成WPS
  13. 嵌入式c语言编程,嵌入式C语言编程集萃.pdf
  14. 西门子g120变频器接线图_西门子变频器G120应用技术手册
  15. java面试英语自我介绍_程序员面试英文自我介绍
  16. HTML系列之文本格式化标签
  17. java word 颜色设置_如何为Word文档在Java中的背景颜色
  18. 安装python报错_安装python包时报错
  19. html超链接 新弹出窗口 和字体颜色
  20. MindManager2022高效好用办公思维导图MindManager

热门文章

  1. 金蝶kis标准版如何修改服务器地址,金蝶kis怎么设置服务器地址
  2. 首届长三角青少年人工智能擂台赛全记录(YOLOv5+Win10+Anaconda+Pycharm+ModelArts)
  3. The Ten Commandments(摩西十诫)
  4. 中国黑客档案:黑客近景写真(1)
  5. 【后台端】OA办公后台管理系统高保真Axure原型模板
  6. 分类整理目前国内比较著名的B2C网站[转]
  7. 手把手教你输出1000到2000之间的闰年
  8. CSS常用属性-3.1字体文字-1.字体font-family-2.尺寸font-size-3.样式font-style-4.粗细font-weight-5.简写属性font
  9. rem动态计算rem
  10. 1到20的阶乘之和是多少