为什么动/静态语言之间有不可逾越的鸿沟
我经常听到有人纠结地问我:
“为什么 aardio 不能像 Python 那样怎么怎么写代码”,
“为什么 aardio 不能像 C/C++ 那样怎么怎么写代码”
可是他们从来没有想过:
Python 就不可能百分百地像 C/C++ 那样写代码。因为 Python 不是 C/C++ ,正如 aardio 不是 Python ,aardio 不是 C/C++ ,你不能把一个编程语言的习惯和思路百分百地套用到另一个编程语言。如果一个编程语言能替代其他编程语言 —— 那我们也就没有必要让 aardio 支持那么多的第三方编程语言了,利用他们而不是变成他们 —— 这是 aardio 的基本原则。
首先我们了解一下,我们在 aardio 中经常提到的静态类型是什么,aardio 里提及的静态类型一般指的是原生静态类型,所以 aardio 操作这种类型的库名称为 raw —— 即原生静态类型。所谓原生静态类型 —— 也就是直接表示内存数据结构的类型,或者说用于表示、存取内存的裸数据,例如 C语言里的一个32位 int 变量值,他在内存里存放的就是 32位4个字节的整数 —— 不含任何额外的附加数据,这是最基本的原生静态类型。
还有一种比较特别的情况,例如 raw.realloc 分配的内存指针,他存放的是原生的数据,返回的指针也完全兼容、适用于需要用到原生指针的场合,但是这种指针稍微有一点特别,在指针指向地址的前面隐藏了一个头结构,该结构的声明如下:
{
INT capacity;//分配的内存容量
INT size;//存储的数据长度
}
raw.realloc 分配内存以后,先占用 8 个字节写了头信息,然后再向后推 8 个字节把这个地址返回为 aardio 中可用的指针值,当然这个“假指针”仍然指向一块空白的、可以自由使用的内存 —— 或者说仍然可以用于存放祼的、原生的静态类型数据。这种对原生内存的扩展是不露痕迹的,大多时候你不知道也不需要知道他在背后的扩展,因为他与原生静态类型完全兼容。
但是操作原生静态类型会很吃力,也充满风险,这就好像让你不使用任何交通工具可以徒步千里 —— 同时也给了你脱离轨道的自由以及大自然随处可遇的风险。
所以现代很多高级动态语言,不再让你直接操作原生的数据类型以及原生指针,在动静态语言之间建立了一道不可逾越的鸿沟。当然这种这种鸿沟的存在有其必要性 —— 正如火车不能允许任意乘客在任意时间下地用双脚原生地奔跑一样。
aardio 这种高级动态语言里的对象比原生静态语言使用的裸的原始数据要复杂得多,例如 aardio 里一个字符串不是你看到的简单的存储了一个字节数组,而且你也不应当读取这个内存指针然后尝试去修改内存里的数据,这里面的机制非常复杂,其实这种保护机制在 C/C++ 里也经常被用到,例如 MFC 里用到的 CString 的字符串是只读的,只有用 GetBuffer 才能返回可写的内存,写完了要用 ReleaseBuffer 再还回去。在 aardio 里是一样的,你要 raw.buffer(str) 才能将字符串转为可读写的内存,而且 tostring(buffer) 可以把 buffer 转为只读的字符串。
如果想打破这些规则 —— 通常情况下这并无任何意义,火车不能允许任意乘客在任意时间下地用双脚原生地奔跑 —— 你说你就是要找到跳下去跑一段再跳回来的方法,意义是什么呢?!
当初我在添加 raw.toPointer() 这个函数的时候曾经犹豫过,默认的 aardio 制定了一套严格的、安全的规则,不允许在不必要的时候获取对象的原生指针,而 raw.toPointer() 打破了这个规则,他可以获得很多原来无法获取的指针,如果滥用这种能力就会带来不必要的麻烦。
例如 raw.buffer 分配的字节数组,他在任何需要的指针的地方都会自动转为指针值,没有特殊的原因,不应当用 raw.toPointer() 取出他的指针地址,raw.buffer 会自动维护指针的生命周期,当对象不再使用时就会释放内存,指针也变得不可用,但如果你在这之前取出了他的指针地址,并且可能把这个地址交给了一些外部API —— 那么指针就指向了无效的内存地址。这在 C/C++ 开发中是常见的、令人肝肠寸断的BUG,你不会喜欢的。
另外原生的内存在变动长度时,指向的地址是可能变动的( 内存里没有一块连续的特权空间让我们可以无限地消耗和增长),用于分配原生内存的 raw.realloc 函数就有这个特征,在重新分配内存时可能修改指针地址。aardio 标准库里的 string.build 内部就是用 raw.realloc 分配内存,string.build 对象在改变字符串长度时也就可能会变动内存地址,但 string.build 只有在被外部 API 使用时才会提供当前指针 —— 所以他提供的指针总是安全和正确的。但如果你试图打破这个规则,提前用 raw.toPointer() 取出他的指针地址,那么这同样是一个危险的、而且完全不必要的操作。
aardio 对象之所以能作为参数用于调用静态语言实现的 API 函数,
是因为 aardio 在其中作了很多隐式的转换,打破了动/静态语言之间原本不可逾越的鸿沟。这很方便,但这种无感的自动转换也会让我们产生错觉,让我们忘记了 aardio 并不是 C/C++,也忘记了不可逾越的鸿沟仍然实际存在。
aardio 也保留了分配、操作、获取原始内存指针的能力,我用 aardio 写了大量的库和软件,我发现 aardio 提供这些底层能力并不会带来问题,只要我们保持这种能力不被滥用即可。
但正因为 aardio 拥有这些能力,淡化了动静态语言间的鸿沟,也让我们在使用 aardio 时可能经常产生某种疑问:“为什么 C/C++ 可以那样……那样……,aardio 不能那样……那样……”,这时候我要提醒你,aardio 不是 C ,更不是 C++ ,真让你百分百按 C/C++ 的规则写代码 —— 你不会喜欢的。要了解 aardio 支持原生静态类型的目的,也只是为了调用和利用静态语言的能力而并不是让自己变成静态语言。
今天我在写 aardio 中添加了 raw.argsPointer 用于获取结构体的二级指针,这在 C代码中很容易,对于本来就使用原生裸数据的原生静态类型语言,提取一个指针是很轻松的事(或者说指针本来就是他们使用的基本语言),无论你是二级、三级 …… 八级指针这都不是事,就像你不使用交通工具在大自然徒步千里 —— 想触摸一下脚下的大地是不是很容易?!但你在奔驰的火车上突然心血来潮 —— 你要触摸一下脚下的大地然后继续享受火车的好处,这就很麻烦不太好实现。
同理, raw.argsPointer 费了不少代码,但我在说明中写明了其实这个 raw.argsPointer 大多时候你用不上,例如你用这个方法得到一个二级指针来操作数组 —— 很可能是把简单的事搞复杂了。你直接传结构体数组更简单 —— 结构体本来就可以随意嵌套,C函数里操作数组也不比操作二级指针更复杂。
大家可以先看看 raw.argsPointer 的范例以及 raw.argsPointer 的源码,这里我给大家看一个新的例子,如果不用 raw.argsPointer 实现同样的功能结果会怎么样,实际上代码会少十倍,不需要 raw.argsPointer,直接传一个结构体数组就可以,aardio / C 语言都可以完美地支持:
var code = /****
typedef struct {
double items[2];
} NUMBERS;
void copyNumberArray(int size,NUMBERS * numbers )
{
for(int i=0;i<size;i++){
for(int j=0;j<2;j++){
numbers[i].items[j] = i*2+j+0.5;
numbers[i].items[j] = i*2+j+0.5;
}
}
}
****/
import tcc;
var c = tcc();
c.compile(code);
class NUMBERS{
double items[2]
}
var numbers = { struct items[2] ={ NUMBERS() } }
c.copyNumberArray(2,numbers) ;
import console;
console.dumpJson( numbers.items );
console.pause();
这是最简单的写法,也是最高效的写法,并不需要任何复杂的技术( 或者 raw.argsPointer 这样复杂的库 )。使用任何编程语言,我们更多地去适应他的规则利用好他的优势,通常比过多消耗精力在打破语言限制上面的回报更大。C/C++ 虽然很强大,但如果我在 aardio 中随便提出几个开源小功能库,让你用 C/C++ 实现一下,我猜你不一定能轻松写出来,把注意力放在语言的限制上 —— 通常是无意义的。
如果有一些代码用 C/C++ 写起来很轻松,用 aardio 写起来很复杂,你完全可以直接用 C/C++ 来写,raw.argsPointer 就用到了一个 C语言写的函数,这个函数只有一句代码,然后我调用 tcc 编译了一下,生成了一个 2KB 的DLL,再用下面的代码将 DLL 直接内存嵌入到 aardio 中:
..raw.loadDll($'~\lib\raw\argsPointer\.res\argsPointer.dll',,'cdecl');
大家会介意我实现的这个函数用到了 C代码吗?我相信没有人介意这一点,aardio 可以混合非常多的第三方编程语言的组件 —— 我们要把这种优势用起来。
当然并不是说 raw.argsPointer 就绝对不可以使用,当你清楚了我上面所描述的细节,并可以肯定你有必要使用 raw.argsPointer,那你就可以用他。对 raw.toPointer() 也一样,当你清楚你自己在做什么,那你也可以用他。毕竟你写出来的程序是你自己的 —— aardio 给了你最大的自由度,相比其他高级动态语言而言, aardio 操作原生类型应当是最自由的吧?或者你还想更自由?!