4.0 结构体对象的封包方案

4.1 正确选择在语言中表示结构体的类型

在完全编译型语言中用struct/class关键字所表示的结构体确实是最简化的方案(其成员采用内存地址给出,访问则是T(0)开销),但在脚本语言中(带运行时解析特性的)却不是这样,它们的类实例化的对象是采用hash表结构来进行存储和访问,并且实例化一个对象并不仅仅只是创建了类中成员的那些数据类型(还包括一些隐藏的反射信息,继承信息等),所以脚本语言的运行时对象开销并不是T0级别;

故我这里想说的是在你确定好那门编程语言的结构体表示时,除了语言所表示的类跟对象外还可以考虑含键值对的关联数组,下面是一个性能测试来对比在PHP中用class创建结构体和array创建结构体

<?php

class A
{
  function __construct()
  {
    $this->a = 0;
    $this->b = '';
  }
}

class B
{
  function __construct()
  {
    $this->aa = new A();
    $this->b = array_fill(0, 1, '');
  }
}

class C
{
  function __construct()
  {
    $this->aaa = array_fill(0, 2, new A());
    $this->bbb = array_fill(0, 1, new B());
  }
}

function new_A()
{
  return [
    'a' => 0,
    'b' => ''
  ];
}

function new_B()
{
  return [
    'aa' => new_A(),
    'bb' => array_fill(0, 1, '')
  ];
}

function new_C()
{
  return [
    'aaa' => array_fill(0, 2, new_A()),
    'bbb' => array_fill(0, 1, new_B())
  ];
}

// --- 1. 创建多个对象的性能测试 ---

// 测试 new C() 创建多个对象
$start = microtime(true);
for ($i = 0; $i<1000000; $i++){
  $obj = new C();
}
$end = microtime(true);
$timeCreateObject = $end-$start;

// 测试 new_C() 创建多个数组对象
$start = microtime(true);
for ($i = 0; $i<1000000; $i++){
  $obj = new_C();
}
$end = microtime(true);
$timeCreateArray = $end-$start;

// --- 2. 访问成员的性能测试 ---

// 访问 C 类对象成员
$cObj = new C();
$start = microtime(true);
for ($i = 0; $i<1000000; $i++){
  $aaa = $cObj->aaa;
}
$end = microtime(true);
$timeAccessObject = $end-$start;

// 访问 new_C() 返回数组成员
$cArray = new_C();
$start = microtime(true);
for ($i = 0; $i<1000000; $i++){
  $aaa = $cArray['aaa'];
}
$end = microtime(true);
$timeAccessArray = $end-$start;

// --- 输出结果 ---

echo "Time to create objects using new C(): ".$timeCreateObject." seconds\n";
echo "Time to create objects using new_C(): ".$timeCreateArray." seconds\n";

echo "Time to access object member (new C()): ".$timeAccessObject." seconds\n";
echo "Time to access array member (new_C()): ".$timeAccessArray." seconds\n";
Time to create objects using new C(): 0.50881600379944 seconds
Time to create objects using new_C(): 0.2998321056366 seconds
Time to access object member (new C()): 0.020484924316406 seconds
Time to access array member (new_C()): 0.019924879074097 seconds

虽然1000000次的性能差距不大,但也很容易看出确实是array形式更胜一筹,我一开始也想说要不就选数组版吧,

不过考虑到IDE识别成员的易用性,最终成品还是选择了对象版

PS:array_fill()该函数用更短的Arr()封装。

 

4.2 对其成员展开式生成专用FB方法

先来个简单的结构体看看【PHP、易语言】的情况

【PHP】
<?php
include 'FB.php';

class A
{
  function __construct()
  {
    $this->a = T_short;
    $this->b = T_String;
  }
}

$o = new A;
$o->a = 123;
$o->b = 'hello';
echo $o->a, " | ", $o->b; //123 | hello

【易语言】


对于编译型语言而言,其在编译后结构体的数据类型等信息是完全丢失的了(易语言仅支持在支持库层面对基本数据类型在运行时进行识别,而结构体对于支持库确实是无能为力),但PHP这种动态语言它依旧可以采用多层遍历等方式去动态提取一个关联数组、类成员的相关key值并在运行时判断它们的类型信息(即便可以要完整写一个通用对象的解析本身的难度和开销就不好说了~)

那我们不可能都保证所有语言都能按照PHP那样去搞解析吧,那如果在不存档一个类的元信息(也就是需要对结构体打表)我们还能否正确从封包字节集的序列化中还原一个结构体?(并且可确保所有语言都通用且依然保持最佳性能开销!)

这里我给该方案取了个名字“对其成员展开式生成专用FB模板方法”,正如此描述,我对每一个类专门生成一个特定的FB封包代码不就可以了吗,在代码中我是可以知道那些成员的类型的以及包括如何处理,比如对A有如下代码FB_A和deFB_A的函数生成

【易语言】

子程序名 返回值类型 公开 备 注
FB_A 字节集    
参数名 类 型 参考 可空 数组 备 注
o A      
返回 (FB (o.a, o.b))

子程序名 返回值类型 公开 备 注
deFB_A      
参数名 类 型 参考 可空 数组 备 注
p 字节集      
o A      
deFB (p, o.a, o.b)

子程序名 返回值类型 公开 备 注
__启动窗口_创建完毕      
变量名 类 型 静态 数组 备 注
o A      
o_ A      
fb 字节集      
o.a = 123
o.b = “hello”
fb = FB_A (o)
调试输出 (fb//字节集:23{2,0,0,0,16,0,0,0,18,0,0,0,23,0,0,0,123,0,104,101,108,108,111}

deFB_A (fb, o_)
调试输出 (o_.a, o_.b) //123 | “hello”


【PHP】
<?php
include 'FB.php';

class A
{
  function __construct()
  {
    $this->a = T_short;
    $this->b = T_String;
  }
}
function FB_A(&$o)
{
  return FB(i16($o->a), $o->b);
}
function deFB_A(&$p, &$o)
{
  deFB($p, $o->a, $o->b);
}

$o = new A;
$o->a = 123;
$o->b = 'hello';

$fb_a = FB_A($o);
echo jzjj($fb_a);

$o_ = new A;
deFB_A($fb_a, $o_);
var_dump($o_);

Bytes:23{2,0,0,0,16,0,0,0,18,0,0,0,23,0,0,0,123,0,104,101,108,108,111}
array(2) {
  ["a"]=>
  int(123)
  ["b"]=>
  string(5) "hello"
}

PHP这里显示int对吗?其实是对的,之前我们一直提到过在PHP内部表示的整型都是统一采用8字节的,关键看对封包的存储(采用i16生成16位字节集)和 解析(直接根据结构体成员赋予的初值给的是有符号和无符号)上

这里我不得不提一下,"T_"给对象专用的类型表示常量(在FB.php中记载)

const T_char = -1;
const T_short = -2;
const T_int = -4;
const T_int64 = -8;
const T_uchar = 1;
const T_ushort = 2;
const T_uint = 4;
const T_bool = false;
const T_float = 4.0;
const T_double = 8.0;
const T_String = '';
const T_Bytes = '';

这些类型初值对于整型而言其正负数有表示无符号和有符号的意义,实际上在解包上它与常规数据类型中的负一和正一的处理是一致的

核心还是deFB中的处理

从一开始我就已经设想到在结构体类中的成员也会这样去使用!——不由得夸一下自己的天才设计,哈哈!

 

4.3 含嵌套结构体的表示方案

我先上例子(这里B::bb采用了重定义数组个数的函数Arr,这里表示是含初始1个字节集成员的字节集数组)

【PHP】
<?php
include 'FB.php';

class A
{
  function __construct()
  {
    $this->a = T_short;
    $this->b = T_String;
  }
}
class B
{
  function __construct()
  {
    $this->aa = new A;
    $this->bb = Arr(T_Bytes, 1);
  }
}

function FB_A(&$o)
{
  return FB(i16($o->a), $o->b);
}

function FB_B(&$o)
{
  return FB(FB_A($o->aa), $o->bb);
}

function deFB_A(&$p, &$o)
{
  deFB($p, $o->a, $o->b);
}

function deFB_B(&$p, &$o)
{
  $p_aa = ''; //这里并非代表字符串,而是提供拼接的缓冲区(如有支持指针或引用的语言应该为其引用该长度的视图)
  deFB($p, $p_aa, $o->bb);
  deFB_A($p_aa, $o->aa);
}

$o = new B;
$o->aa->a = 123;
$o->aa->b = 'hello';
$o->bb = ['world','_hello'];
$fb_b = FB_B($o);

$o_ = new B;
deFB_B($fb_b, $o_);
var_dump($o_);


class B#3 (2) {
  public $aa =>
  class A#4 (2) {
    public $a => int(123)
    public $b => string(5) "hello"
  }
  public $bb =>
  array(2) {
    [0] => string(5) "world"
    [1] => string(6) "_hello"
  }
}

是的,如果B嵌套了结构体那么只好借助原先FB_A进行封包好然后作为B的成员再进行单独的一部分参数封包,对于解包也是同理;

但解包这里其实还有优化的地方(只是对于PHP没有指针则无能为力),那就是deFB_B的第一层解包得到的可以不必是字节集类型的p_aa,因为一旦给出p_aa是字节集那么就不可避免的会产生从投入封包数据中的内存复制,实际上这种内存复制是完全不需要的我之前有说过FB的设计是可以直接投入首指针就能够确定地遍历完整的子封包数据了

在编译型语言中还可以有如下优化(deFB的底层实现需要同时判别和支持指针型和字节集buf的投入,由于易语言本身没有指针类型,但有子程序指针故可拿来充当指针型——注意不能搞成整数型,因为没法区分要导出的是存储中的整数值还是把仅把所在地址保存给到上级解包变量

数据类型名 公开 备 注
A    
成员名 类 型 传址 数组 备 注
a 短整数型    
b 文本型    

数据类型名 公开 备 注
B    
成员名 类 型 传址 数组 备 注
aa A    
bb 字节集   1

窗口程序集名 保 留   保 留 备 注
程序集1      
子程序名 返回值类型 公开 备 注
FB_A 字节集    
参数名 类 型 参考 可空 数组 备 注
o A        
返回 (FB (o.a, o.b))

子程序名 返回值类型 公开 备 注
deFB_A      
参数名 类 型 参考 可空 数组 备 注
p 子程序指针        
o A        
deFB (p, o.a, o.b)

子程序名 返回值类型 公开 备 注
FB_B 字节集    
参数名 类 型 参考 可空 数组 备 注
o B        
返回 (FB (FB_A (o.aa), o.bb))

子程序名 返回值类型 公开 备 注
deFB_B      
参数名 类 型 参考 可空 数组 备 注
p 子程序指针        
o B        
变量名 类 型 静态 数组 备 注
p_aa 子程序指针      
deFB (p, p_aa, o.bb)
deFB_A (p_aa, o.aa)


子程序名 返回值类型 公开 备 注
__启动窗口_创建完毕      
变量名 类 型 静态 数组 备 注
o B      
o_ B      
zz 字节集    
fb 字节集      
o.aa.a = 123
o.aa.b = “hello”
加入成员 (zz, 到字节集 (“world”))
加入成员 (zz, 到字节集 (“_hello”))
o.bb = zz

fb = FB_B (o)
deFB_B (pFB (fb), o_)
调试输出 (o_.aa.a, o_.aa.b)
调试输出 (o_.bb)
* 123 | “hello”
* 数组:2{字节集:5{119,111,114,108,100},字节集:6{95,104,101,108,108,111}}

PS:易语言这里我是先用pFB取得fb这个字节集的指针,之后deFB_B中第一层deFB取得的是o.aa的在封包数据中的内存指针,最后再投入deFB_A来解包得出o.aa

 

4.4 复合结构体中含嵌套结构体数组的成员

先来看给出的例子结构体

其生成的封包方案需要在生成函数中实现将结构体的数组转换为一个个序列化过后的字节集数组,然后最后再进行对字节集数组的封包(我们的FB()函数本身就已经支持对字节集数组的封包)

【PHP】

function FB_C(&$o)
{
  $n = count($o['aaa']);
  $o_aaa = Arr('', $n);
  for ($i = 0; $i<$n; $i++){
    $o_aaa[$i] = FB_A($o['aaa'][$i]);
  }
  $n = count($o['bbb']);
  $o_bbb = Arr('', $n);
  for ($i = 0; $i<$n; $i++){
    $o_bbb[$i] = FB_B($o['bbb'][$i]);
  }
  return FB($o_aaa, $o_bbb);
}
【易语言】

子程序名 返回值类型 公开 备 注
FB_C 字节集    
参数名 类 型 参考 可空 数组 备 注
o C        
变量名 类 型 静态 数组 备 注
o_aaa 字节集    
i 整数型      
n 整数型      
o_bbb 字节集    
n = 取数组成员数 (o.aaa)
重定义数组 (o_aaa, 假, n)
计次循环首 (n, i)
o_aaa [i]FB_A (o.aaa [i])
计次循环尾 ()
n = 取数组成员数 (o.bbb)
重定义数组 (o_bbb, 假, n)
计次循环首 (n, i)
o_bbb [i]FB_B (o.bbb [i])
计次循环尾 ()
返回 (FB (o_aaa, o_bbb))


解包的话需要先使用FB_n提取该对象所在封包字节集的个数(那如何得知一个对象是数组呢?我说过我们在生成代码的预处理阶段其实我们就是神,我们能够得知它的类+型去知道这是一个数组然后才生成的代码),得到个数后就可以进行这种结构体的类型的数组重定义了,然后一个个投入在遍历中用原先就已经写好的相关生成函数进行解析

【PHP】

function deFB_C(&$p, &$o)
{
  $p_aaa = $p_bbb = '';
  deFB($p, $p_aaa, $p_bbb);
  $n = gFB_n($p_aaa);
  $o->aaa = Arr(new A, $n);
  for ($i = 0; $i<$n; $i++){
    deFB_A(gFB($p_aaa, $i), $o->aaa[$i]);
  }
  $n = gFB_n($p_bbb);
  $o->bbb = Arr(new B, $n);
  for ($i = 0; $i<$n; $i++){
    deFB_B(gFB($p_bbb, $i), $o->bbb[$i]);
  }
}
【易语言】
子程序名 返回值类型 公开 备 注
deFB_C      
参数名 类 型 参考 可空 数组 备 注
p 子程序指针        
o C        
变量名 类 型 静态 数组 备 注
n 整数型      
i 整数型      
p_aaa 子程序指针      
p_bbb 子程序指针      
deFB (p, p_aaa, p_bbb)
n = pFB_n (p_aaa)
重定义数组 (o.aaa, 假, n)
计次循环首 (n, i)
deFB_A (pFB_p (p_aaa, i), o.aaa [i])
计次循环尾 ()
n = pFB_n (p_bbb)
重定义数组 (o.bbb, 假, n)
计次循环首 (n, i)
deFB_B (pFB_p (p_bbb, i), o.bbb [i])
计次循环尾 ()

使用例子

<?php
include 'FB_C.php';

TEST_C();

function TEST_C()
{
  $o = new C;
  $o->aaa[0]->a = 123;
  $o->aaa[0]->b = 'hello';
  $o->bbb[0]->bb = ['world','_hello'];
  $fb_c = FB_C($o);

  echo jzjj($fb_c);

  $o_ = new C;
  deFB_C($fb_c, $o_);
  var_dump($o_);
}
Bytes:154{2,0,0,0,16,0,0,0,79,0,0,0,154,0,0,0,2,0,0,0,16,0,0,0,39,0,0,0,62,0,0,0,2,0,0,
0,16,0,0,0,18,0,0,0,23,0,0,0,123,0,104,101,108,108,111,2,0,0,0,16,0,0,0,18,0,0,0,23,0,0,
0,123,0,104,101,108,108,111,98,1,0,0,0,12,0,0,0,74,0,0,0,2,0,0,0,16,0,0,0,34,0,0,0,62,0,
0,0,2,0,0,0,16,0,0,0,18,0,0,0,18,0,0,0,254,255,2,0,0,0,16,0,0,0,21,0,0,0,27,0,0,0,119,111,
114,108,100,95,104,101,108,108,111,98,98}
class C#5 (2) {
  public $aaa =>
  array(2) {
    [0] =>
    class A#9 (2) {
      public $a =>
      int(123)
      public $b =>
      string(5) "hello"
    }
    [1] =>
    class A#9 (2) {
      public $a =>
      int(123)
      public $b =>
      string(5) "hello"
    }
  }
  public $bbb =>
  array(1) {
    [0] =>
    class B#6 (2) {
      public $aa =>
      class A#10 (2) {
        ...
      }
      public $bb =>
      array(2) {
        ...
      }
    }
  }
}

我们可以把这个字节集给易语言那边进行反序列化(这里pFB去取得字节集的指针,那是因为我们约定这些解包都统一投入"子程序指针"这个类型了,当然你怕麻烦也可以独自改为传入字节集,不过deFB_B这些由于是中间类型要传入字节集那么必然会有内部取子字节集时的二次拷贝):

非常完美!

 

4.5 使用AI辅助生成目标语言的结构体声明文件

你应该也注意到了,手写 FB_结构/deFB_结构 等其实十分繁琐,一旦结构体嵌套的层次一多手写变得不那么现实,所以我们需要从一个已知的声明源文件(这里我以C语言头文件去写结构体声明作为基底)让AI去翻译给出生成目标语言结构体文件的代码的编写生成代码(我这句话意思是写生成器代码的工具脚本这个活让AI去做)

目前我已经独自完成了易语言和PHP的生成器工具,我下面以C语言转目标PHP为例演示一下用法:
(需配置PHP运行时环境php.exe,解压代码仓库中的[php-core.zip]将其路径写入PATH环境变量即可使用,当然你也可以用其他语言实现该类似的工具)

  1. CStructToPhp.php脚本是由AI生成的


  2. 使用命令行脚本生成后得到FB_C.php


  3. 生成的代码内容


  4. 在你的项目中使用该C类(依赖的引用只需FB.php、FB_C.php)