新一代脚本语言引擎Cx -- 应用之AutoCAD二次开发 (1)
“十年磨一剑”,Cx经历十年的研究、沉淀、设计。。。
01. 拥有ARX的强大功能,具备LISP的灵活、简便、动态等特性
02. 非常方便与其他程序的沟通和调用DLL功能
03. 支持反应器
04. 高度动态
05. 尽力静态
06. 简洁的数据操作
07. 支持DCL, MFC
08. 支持ForEach, map
09. 良好容错、除错能力
10. 支持(自)覆盖 / 热替换
11. 支持正则表达式
12. 支持多线程编程、并行计算
(2):
13. 实时可控的动态数据处理方式,高级指针、GC
14. 内建大量数据类型及处理方法
15. 支持超高精度整数和超高精度实数
16. 类定义中的“简化型”多重继承、运算符重载、仿函数
17. 模块化(#Package)编程
18. 参考(Reference)/映射(Refect)型 变量和数据
19. 传址参数、署名参数(缺省参数)的函数定义
20. 参数包(Valist)、闭包(Closure)、子函数
21. 窗口函数的消息处理、事件反应、虚拟调用
22. 方便编写键盘重设置(键盘宏)、成倍提高电脑操作速度
23. 支持小型2D-CAM系统,方便编写CNC加工的应用程序
24. 装载、命令行操作
25. 其他说明
(3): (待续)
一种新的强大的脚本语言引擎,Cx语言, 拥有C/C++语言的强大功能,又具备普通脚本语言的灵活、便利、简洁、动态、非编译等特性,已成功用在AutoCAD、ZWCAD和办公室软件WPS等,成为其强大、灵活的二次开发程序语言。本文介绍用在AutoCAD里的Cx。
01. 拥有ARX的强大功能,具备LISP的灵活、简便、动态等特性
对于AutoCAD,其ObjectARX(以下简称ARX)的编译型C/C++程序,功能可以非常强大,但是,作为编译型的C/+C++程序语言,门槛高、编程繁琐、编译扰人、共享不易、版本敏感等,让不少人转到C#和JAVA,但是C/C++中有很多很值得人们珍惜和学习的东西!这些年来,AutoCAD每年升级一次,原有的ARX应用程序,就面临升级/版本兼容问题,这既会严重影响人们对CAD升级的欲望,也影响人们对ARX应用程序欲望。
Cx以ARX方式结合到AutoCAD中。Cx程序的书写格式、运作方式、数据结构等都非常类似C/C++程序,支持包括指针、回调函数、位域、MFC、Win32 API等在内的C/c++大部分数据结构和功能,也可以进行内存直接处理的非常底层的操作。
与C/C++不同,Cx的内存处理基本上是自动的。与.NET的内存托管、JAVA的GC不同,Cx的内存处理是实时可控的。Cx里可以存在多个变量/结点同时拥有同一动态数据,却不会引发死循环而造成内存永久不能释放的问题。
文件句柄、资源句柄、CAD实体对象句柄等,如果Cx程序没有显示Close或释放,Cx会在拥有句柄的变量/结点的作用域失效时、或数值要被覆盖时,自动关闭或释放这些句柄。象句柄的打开与关闭、多线程中数据同步锁的取得与释放等需要成对的操作,Cx会严格控制,一旦程序异常中断,Cx会自动进行善后处理,没有及时关闭或释放的,会将其关闭或释放,这对于脚本语言来说非常重要。如果CAD实体没有及时close(),对CAD来说基本上是致命的崩溃、甚至烟消云散。
作为脚本语言,Cx同时又具备普通脚本语言的各种高级特性,与AutoLISP一样,很少受CAD平台的版本影响,编程、装载、调用、功能共享等,都非常方便。在功能、表达能力、运行速度等方面,CX显著超越AutoLISP。
在数据类型方面,Cx既支持C/C++方式的强制类型数据,也支持普通脚本语言的泛型数据。
先看看下面一个例子,这个例子取自网络牛人“雨中飞燕” 提供的求1000阶乘算法的C代码,通过改写函数头,long前加一个点,就成为Cx语言程序了:
#define Nx 1000
public function [C:C00]()
{
.long s[Nx]={1,1}, n=Nx, t=2, a=1, b=0;
for (; a <= *s || (++t <= 1000 ? (b=0, a=1) : 0); (*s == a++ && b) ? (*s)++ : 0)
s[a] = (b += s[a] * t) % 10000, b /= 10000;
for (printf("%d", s[*s]); --*s > 0; ) printf("%04d", s[*s]);
}
相信绝大多数的脚本语言无法书写表达如此精练的程序(不是可读性^_^)。这个例子足以显示Cx语言良好的表达能力、与C的亲近、相似程度。
Cx定义函数,以关键字 def 或 function 标识,函数名称可以用任何字符,当然包括使用中文,当使用"不规范字符"的名称时,用[], ‘’,或""界定,上面定义的这个函数,用[ ]界定,名称是 C:C00,如果函数定义用 关键字public前置修饰,那么,定义的这个函数,会在CAD的命令队列中注册一个命令C00,这样,操作者在CAD命令行中输入 C00时,就会启动相应的函数。这点,非常类似AutoLISP函数的定义。
ARX中的大部分类定义都可以映射到Cx中,在Cx中使用这些类,宛如在C/C++中使用,因此,熟悉C/C++,尤其熟悉ARX程序的人,对Cx程序应该不会陌生。
Cx操作CAD可以使用多种方式编程: ARX方式, ADS / LISP方式, ActiveX / VBA方式,C动态编译方式等,其中以ARX方式编程运行效率最高,如果不是操作CAD,而是纯粹的数值计算,C的动态编译方式的运行效率最高,它基本上等效C的静态编译程序。
下面以同时缩放5万个圆实体半径为测试实例,展示CX不同的编程方式。电脑配置:XP, CPU: Q6600 @2.4G HZ(4 核),RAM: 2G。这也是本文所有测试使用的电脑配置。预先画好的5万个CIRCLE实体,启动相应命令时,选取该5万CIRCLE,再输入缩放值,比如 2.0。运行结果如下表:
---------------------------------------------------------------
程序语言 耗时(毫秒) 比较
---------------------------------------------------------------
Cx (Arx 普通方式) 297 1
Cx (Arx 优化方式) 219 0.74
Cx (Ads 方式 ) 3250 10.94
Cx (ActiveX 方式) 828 2.79
Cx (C的动态编译方式) 2766 9.31
AutoLISP 4109 13.84
ARX (C/C++ 编译型) 94 0.32
ADS (C 编译型) 2687 9.05
---------------------------------------------------------------
无论是ADS的静态编译型的C程序,或动态编译型的C程序,还是AutoLISP程序,对CAD实体的操作,都是通过动态生成resbuf数据链间接操作,因此相当耗时,C程序的高效性在这里无法表现出来, 如果只是数值运算,编译型程序会展现很好的运行效率,象上面第一个程序C:C00的求1000阶乘中的循环部分,经过测试,得到如下结果:
-------------------------------------------------------------------------------------
程序语言 耗时(毫秒)
--------------------------------------------------------------------
Cx (普通方式) 375
Cx (TrySpeed优化方式) 172
Cx (动态编译方式) <10
C (静态编译) <10
---------------------------------------------------------
可以看出,这里的动态编译的程序,其运行速度与传统静态编译型的C程序没有什么差别,显著超越非编译型的脚本语言。
ActiveX方式操作CAD,没有牵涉数据链,因此运行效率较高,但是ActiveX接口本身存在一定的效率损耗。
Cx程序展示出高度的灵活性和适应性,能够以各种方式混合编程,既可以根据实际运行效率的需要,也可以根据编程者自身的喜好、掌握的技能、水平状况编写相应的程序。无论ARX编程者,还是VBA编程者,或AutoLISP编程者,在Cx里都可以找到自己熟悉的编程方式。
优化后的Cx程序,其操作CAD的运行速度达到ARX C/C++编译型程序的一半,而其程序码的编写比C/C++程序要来得简洁。AutoLISP程序是所有程序中 运行最慢的。
各种程序码如下:
1)ARX 方式, 耗时 297 ms (毫秒) 或 219 毫秒:
public function [C:C01]()
{
ss = ssget( buildlist(0, "CIRCLE"));
if (!ss) return;
sc = getreal("\nScale<2.0> =");
if (!sc) sc = 2.0;
t1 = GetCurrentTime();
for (en in ss) {
acdbOpenObject(pEnt, en, AcDb::kForWrite);
pEnt->setRadius( pEnt->radius() * sc);
}
printf("Time = %d\n", GetCurrentTime() - t1);
}
该程序处理5万个CIRCLE运行耗时297毫秒。
用Cx提供的AcDbObjectOpenArray类,把选择集中的所有实体一次性open(),再逐一操作实体,最后一次性close()所有实体。这样可以进一步提高Cx程序的运行效率,仅仅耗时219毫秒。
public function [C:C02]()
{
ss = ssget( buildlist(0, "CIRCLE"));
if (!ss) return;
sc = getreal("\nScale<2.0> =");
if (!sc) sc = 2.0;
t1 = GetCurrentTime();
A = new AcDbObjectOpenArray(ss.len);
A.appendSS(ss, AcDb::kForWrite);
for(pEnt in A) {
pEnt->setRadius( pEnt->radius() * sc);
}
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
}
2)ADS 方式, 耗时 3250 ms (毫秒):
public function [C:C03]()
{
ss = ssget( buildlist(0, "CIRCLE"));
if (!ss) return;
sc = getreal("\nScale<2.0> =");
if (!sc) sc = 2.0;
t1 = GetCurrentTime();
for(en in ss) {
eg = entget(en);
eg.SeekType(40)->Value *= sc;
entmod(eg);
}
printf("Time = %d\n", GetCurrentTime() - t1);
}
3)ActiveX 方式, 耗时 828 ms (毫秒):
public function [C:C04]()
{
ss = ssget( buildlist(0, "CIRCLE"));
if (!ss) return;
sc = getreal("\nScale<2.0> =");
if (!sc) sc = 2.0;
t1 = GetCurrentTime();
for(en in ss) {
pDisp = en.Dispatch();
pDisp.Radius = pDisp.Radius * sc;
}
printf("Time = %d\n", GetCurrentTime() - t1);
}
4)C的动态编译方式, 耗时 2766 ms (毫秒):
public function [C:C05]()
{
cc = new CCode();
cc.__add_include_path( CCodeMainPath() + "ADS\\");
cc.__add_ADS();
cc.__compile( {%%
#include <windows.h>
#include "ads.h"
#include "adscodes.h"
void ScaleCircles()
{
struct resbuf *filter=NULL, *eg, *p;
ads_name ss, en;
double sc;
long slen, i;
int t1, res;
filter = ads_buildlist(RTDXF0, "CIRCLE", 0);
res = ads_ssget(NULL, NULL, NULL, filter, ss);
ads_relrb(filter);
if (RTNORM != res) return;
res = ads_getreal("\nScale<2.0> = ", &sc);
if (RTNORM != res) sc = 2.0;
t1 = GetCurrentTime();
ads_sslength(ss, &slen);
for (i=0; i<slen; i++) {
ads_ssname(ss, i, en);
eg = ads_entget(en);
p = eg;
while(p) {
if (p->restype == 40) {
p->resval.rreal *= sc;
break;
}
p = p->rbnext;
}
ads_entmod(eg);
ads_relrb(eg);
}
ads_ssfree(ss);
ads_printf("T1=%d\n", GetCurrentTime() - t1);
}
%%} );
cc.__declare {
void ScaleCircles();
};
cc.ScaleCircles();
}
5)AutoLISP程序,耗时4109 ms (毫秒):
(defun C:L01 (/ sc i en eg a1)
(setq filter (list (cons 0 "CIRCLE")))
(setq ss (ssget filter))
(if ss (progn
(setq sc (getreal "Scale <2.0> = "))
(if (not sc) (setq sc 2.0))
(cascript "t1 = GetCurrentTime(); ")
(setq sslen (sslength ss) i 0)
(while (< i sslen)
(setq en (ssname ss i)
i (1+ i)
eg (entget en)
a1 (assoc 40 eg)
eg (subst (cons 40 (* (cdr a1) sc)) a1 eg)
)
(entmod eg)
)
(cascript " printf(_'Time = %d (ms)\n', GetCurrentTime() - t1);")
))
(princ)
)
6)C/C++ (ARX)编译程序,耗时 94 ms (毫秒):
void Arx_ScaleCircles()
{
Acad::ErrorStatus es;
struct resbuf *filter=NULL;
ads_name ss, en;
AcDbObjectId objId;
AcDbCircle *pEnt;
cat_real sc;
int res, t1;
long slen, k;
filter = ads_buildlist(RTDXF0, "CIRCLE", 0);
res = ads_ssget(NULL, NULL, NULL, filter, ss);
ads_relrb(filter);
if (RTNORM != res) return;
res = ads_getreal("\nScale <2.0> = ", &sc);
if (RTNORM != res) sc = 2.0;
t1 = GetCurrentTime();
ads_sslength(ss, &slen);
for (k=0; k<slen; k++) {
res = ads_ssname(ss, k, en);
if (res == RTNORM) {
es = acdbGetObjectId(objId, en);
if (es == Acad::eOk) {
es = acdbOpenAcDbEntity((AcDbEntity* &)pEnt, objId, AcDb::kForWrite);
if (es == Acad::eOk) {
pEnt->setRadius( sc * pEnt->radius());
pEnt->close();
}
}
}
}
ads_printf("Time = %d\n", GetCurrentTime() - t1);
}
7)C (ADS)编译程序,耗时 2687 ms (毫秒):
该程序与“4)C的动态编译方式” {%% %%}中的程序完全一样。
再测试,以生成5万条直线为例子,得到的结果与上面的情况基本一样:
------------------------------------------------------
程序语言 耗时(毫秒) 比较
------------------------------------------------------
Cx (ARX方式) 297 1
AutoLISP 1360 4.58
ARX C/C++ 编译型程序 109 0.37
ADS 编译型程序 1032 3.47
------------------------------------------------------
限于篇幅,这里仅仅给出Cx 的ARX方式的程序:
public function [C:C06]() //定义一个CAD命令 C06
{
.AcGePoint3d p1(0,0,0), p2(0, 100, 0);
acdbCurDwg().getBlockTable(pTab, AcDb::kForRead);
pRec = NULL;
pTab.getAt("*Model_Space", pRec, AcDb::kForWrite);
pTab = NULL;
t1 = GetCurrentTime();
repeat( x in 50000) {
p2.x = x;
pEnt = new AcDbLine(p1, p2);
pRec.appendAcDbEntity(pEnt);
}
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
}
02. 非常方便与其他程序的沟通和调用DLL功能
Cx支持包括指针、回调函数在内的绝大部分C/C++数据类型,因此Cx调用普通DLL中的函数、数据非常简便,例如:
myDll = new DLL(“myDll.dll”);
myDll.__declare{
double func1( double a, double b);
void func2( int x);
int varNum;
};
R = myDll.func1(1.0, 2.0);
myDll.func2(123);
++myDll.varNum;
被LISP函数(vl_acad_defun) 注册过的LISP函数,可以被Cx函数调用,比如有LISP函数:
(defun lispFunc(x y)
(+ (* 10 x) y)
)
(vl_acad_defun ‘lispFunc)
这样就可以在Cx程序里直接调用(以操作符 “!”标识调用的是LISP函数):
Val = lispFunc!(2, 3);
以public 标识的Cx函数,会在LISP空间注册相应的函数,让LISP程序直接调用。因为Cx程序往往是定义在各自的命名空间,因此,LISP里注册Cx函数时,会在函数名前加空间名的前缀,比如:
//Cx 程序:
#package pak_1
public function cxFunc(x, y)
{
return 10 *x + y;
}
#package _end_
;LISP 程序:
(defun test()
(setq val (Pak_1^cxFunc 2 3))
)
但是,如果函数名是前缀C:,则不做追加前缀处理,而是与LISP语言一样,直接注册成CAD命令型函数。
Cx支持操作ActiveX接口,既可以通过该接口操作CAD,比如对上面对CIRCLE实体进行半径缩放的例子(C:04),也可以通过该接口,启动、连接诸如Excel.Application,或电脑系统中大量的存在的ActiveX构件,比如:
public function [C:C07]()
{
for (x in GetObject( "WinMgmts:").InstancesOf( "Win32_DiskDrive")){
MsgBox(x.Model, L"Information of Win32_DiskDrive");
}
}
显然,通过ActiveX接口,让CAD与Excel等软件交换数据是很方便的。
下面给一个例子,展示如何用Cx的回调函数为参数调用 Win32 API 的函数,具体是枚举AutoCAD 里 acadbtn.dll中所有图标BITMAP/ICON资源的名字,并且屏幕显示前十个名字。在声明Win32 API函数时,回调函数的参数以 void * 类型声明。 Cx支持类的成员函数构建回调函数,这样的回调函数类似C#中的delegate,能够调用对象里封装的数据成员。与C/C++不同,Cx里取得函数myFunc的地址是通过&.myFunc()格式。
//
#define RT_BITMAP 2
#define RT_ICON 3
//
#package myDemo
//
public function [C:C08] ()
{
Enum = new CEnumResource("acadbtn.dll");
if (!Enum.Start()) return;
prinn("\nBitmap Names List:\n");
for (x in Enum.m_arrBitmap, 10) prinn(x);
prinn("\nIcon Names List:\n");
for (x in Enum.m_arrIcon, 10) prinn(x);
}
//
public class CEnumResource
{
def CEnumResource( sourceFile);
def Start();
def EnumResTypeProc (HMODULE hInst, LPSTR pszType, LPARAM lParam);
def EnumResNameProc (HMODULE hInst, LPSTR lpszType, LPSTR lpszName,
LPARAM lParam);
DLL m_source;
DLL m_kernel32;
CStringArray m_arrBitmap;
CStringArray m_arrIcon;
__Callback m_TypeProc;
__Callback m_NameProc;
};
//
function CEnumResource::CEnumResource ( sourceFile)
{
m_source = new DLL(sourceFile);
if (!m_source) return;
m_kernel32 = new DLL("kernel32.dll");
m_kernel32.__declare {
BOOL EnumResourceTypesA (HMODULE hInst, void *TypeProc,
LONG lParam);
BOOL EnumResourceNamesA (HMODULE hInst, LPCTSTR pszType,
void *NameProc, LONG lParam);
};
// 构建回调函数
m_TypeProc = new __Callback( &.EnumResTypeProc());
m_NameProc = new __Callback( &.EnumResNameProc());
}
//
function CEnumResource::Start()
{
if (!m_kernel32) return FALSE;
m_kernel32.EnumResourceTypesA( m_source.__GetInstance(),
m_TypeProc, 0);
return TRUE;
}
//
function CEnumResource::EnumResTypeProc (HMODULE hInst, LPSTR pszType,
LPARAM lParam)
{
m_kernel32.EnumResourceNamesA( hInst, pszType, m_NameProc, lParam);
return TRUE;
}
//
function CEnumResource::EnumResNameProc (HMODULE hInst, LPSTR lpszType,
LPSTR lpszName, LPARAM lParam)
{
switch ((int) lpszType) {
case RT_BITMAP:
m_arrBitmap.add(lpszName);
break;
case RT_ICON:
m_arrIcon.add(lpszName);
break;
}
return TRUE;
}
//
#package _end_
//
输入命令C08启动上面定义的函数 C:C08,得到如下输出结果:
Bitmap Names List:
ICON_16_2DOPTIM
ICON_16_3DCLIP
ICON_16_3DCLIPBK
ICON_16_3DCLIPFR
ICON_16_3DCORBIT
ICON_16_3DDISTANCE
ICON_16_3DFACE
ICON_16_3DMESH
ICON_16_3DORBIT
ICON_16_3DPAN
Icon Names List:
上面这段程序中有几个函数、方法定义中,对参数的类型做了强制限制,这是回调函数的需求。
03. 支持反应器
下面展示的例子是建立AcEditorRector型反应器,当该反应器打开(ON)时,一旦完成 OFFSET,COPY,MIRROR,ARRAY或 –ARRAY命令后,该反应器会自动把刚刚生成的所有实体的层、颜色和线型的属性改成DWG当前值:
///
#package myDemo
class MyEdReactor;
///
public function [C:C09]()
{
static var myEd=NULL;
initget("ON OFF");
kw = getkword ("\nEnter an option [ON/OFF] <" +
(myEd ? "OFF" : "ON") + ">: ");
if (!kw) {
kw = myEd ? "OFF" : "ON";
}
if (kw == "ON") {
if (myEd) return;
myED = new MyEdReactor;
prinn(" acting myEdReactor...");
} else {
myED = NULL;
prinn(" myEdReactor delected.");
}
}
///
class MyEdReactor : AcEditorReactor
{
var m_en;
def commandWillStart (cmd) { m_en = entlast(); }
def commandEnded (cmd);
};
///
function MyEdReactor::commandEnded (cmd)
{
ss = sslast(m_en);
if (!ss) return;
switch(cmd) {
case "OFFSET":
case "COPY":
case "MIRROR":
case "ARRAY":
case "-ARRAY":
ss.SetLayer( GetVar( "CLAYER"));
ss.SetColor( GetVar( "CECOLOR"));
ss.SetLtype( GetVar( "CELTYPE"));
break;
}
}
///
#package _end_
///
04. 高度动态
Cx程序可以非常动态,象前面说的C程序的动态编译,后面将要说明的程序覆盖/“热替换”,都是很好用的动态特性。此外,函数名、变量名、类型名等标识符(名称)可以为任何字符组成,甚至可以计算。一般以前置符$作为特殊标识开始,比如:
$[姓名] = “张三”;
$”年龄” = 23;
++${“AB # C & @” + 123};
在可以明确是标识符的地方,比如函数名定义处,前置符$可以省略。
Cx程序码可以动态构建,方式有多种:Lambda函数、普通函数体、字符串等,都可以动态插入运行。对于函数体、Lambda定义体的插入运行,与单独运行这些函数、Lambda定义体不同,比如定义体中的return语句,仅仅起到结束插入体程序运行的作用,不会对当前正在运行的函数产生影响。类似C语言中运行一个{。。。}局部结构的程序,不同的是Cx有带参数,这一点又象宏带入。一旦插入的程序段运行结束,期间产生的局部变量会马上释放。例如,有字符串:
S = “ x = a + b * c”;
可以动态执行:
S.eval();
等效执行字符串里表达的程序。只是这里的运行解释过程会比较消耗时间,如果对字符串里的表达式要求高效运行,可以先进行格式化处理:
F = Formula(S);
再运行:
F.eval();
或
F();
运行效率一般可以提高一个数量级。
Cx的函数定义,支持可变参数,比如,定义一个任意参数个数的加法函数:
function myAdd(A, …)
{
no = ThisParamNo();
for(k=1; k<no; k++) {
A += *ThisParam(k);
}
return A;
}
其中内建函数ThisParamNo()取得当前函数myAdd的参数个数,内建函数ThisParam()则取得当前函数的参数地址/指针,这样,通过表达式:
B = myAdd(1,2,3,4);
变量B得到的值就是:10
05. 尽力静态
Cx支持泛型变量,也支持强制类型声明的变量。明确变量类型,有利于Cx程序在运行过程静态处理,比如对高强度循环过程,通过语句:trySpeed {…} 有可能显著提高程序运行速度:
public function [C:C10]()
{
res = 0i64;
t1 = GetCurrentTime();
for (k=0; k<10000000; k++) {
res += k;
}
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
}
public function [C:C11]()
{
.int k;
.__int64 res=0;
t1 = GetCurrentTime();
trySpeed {
for (k=0; k<10000000; k++) {
res += k;
}
};
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
}
上面定义两个函数,其中C:C10采用泛型方式,而C:C11采用强制类型声明,并且引入程序结构trySpeed{…},运行结果显示,C:C10耗时2234 毫秒,C:C11耗时仅仅719毫秒。可以算出C:C11的运行速度是C:C10的3倍。
Cx语言会对一些语句进行静态优化,比如switch-case语句,因为进行静态优化处理过,其case 匹配过程非常快速,即使存在数以千计的case 语句,也可以在瞬间匹配到位。如果使用传统的if –else if … 语句方式,当case 比较多时,会明显影响运行效率。
06. 简洁的数据操作
Cx中的大部分数据结构,与C/C++中的数据结构是完全一致的,当与C/C++编译成型的Win32 API的DLL模块进行沟通时,数据不需要进行任何转换,比如,Cx语言生成的字符串数组:
mArray = new TSTR[1024];
在内部数据结构分布上与C中的如下数组是完全一样的:
LPTSTR * cArray = new LPTSTR[1024];
因此,当有C的库函数需要这种类型的参数时,可以把mArray直接赋值参数,Cx语言内部不需要做任何数据转换,如:
myDll = new DLL(“abc.dll”);
myDll.__declare {
void xFunc(int a, LPTSTR[] b);
};
myDll.xFunc(123, mArray);
在Cx语言里对mArray数组元素的赋值,要比C里简洁、自在得多,不需要考虑内存泄漏问题,也不会发生内存泄漏,有关内存的释放,Cx语言会自动完成,程序员的编程仅仅是进行简单的赋值操作:
mArray[8] = “ABC”;
mArray[8] = aNewStr;
不要担心mArray[8]处原来是否存在字符串,如果存在,也用不着编程去释放这个字符串,Cx 语言会自动、马上释放。
如果把某数组元素赋予变量v,实际上是让v指向该元素拥有的字符串:
v = mArray[8];
当mArray[8]拥有的字符串更新时,原来的字符串自然失效,变量v的值会自动变为NULL。
调用Win32 API函数时,往往会要求各种struct型参数,有相当部分在Cx内部有预定义,如果没有,可以用Cx程序直接书写,Cx支持位域、位对齐等,效果与C/C++中定义的完全相同。
有C/C++编程经验的人,应该可以从中看出Cx语言程序的简便方式对编程效率、品质的意义。
AutoLISP语言操作CAD,往往通过resbuf 数据链,因为LISP语言的传值特性,对数据链的任何修改,都会引发重构整条数据链,造成明显性能损耗。Cx支持指针,可以修改resbuf 数据链中任意数据结点而不必重构数据链,而且操作非常简单,比如,变量L1中有如下resbuf数据链:
(0 1 2 3 4 5 6)
如果要修改第三个数(从0位算起),比如乘8:
L1[3].value *= 8;
L1中的数据链变为:
(0 1 2 24 4 5 6)
也可以把第三个结点的数据直接赋其他类型数据的值:
L1[3] = “Hello”;
L1中的数据链变为:
(0 1 2 “Hello” 4 5 6)
还可以方便操作resbuf数据结点亚结构中的数据,比如,变量L2中有如下resbuf数据链:
(0 1 2 (11 “X” “Y”) (22 “U” “V”) 3.14)
用如下表达式:
(L2+3)[2].Value += “-ABC”;
就可以把L2改成:
(0 1 2 (11 “X” “Y-ABC”) (22 “U” “V”) 3.14)
Cx仅仅是通过数据指针修改相应亚结点的数据,其他结点的数据完全没有改变。操作很方便吧。
大部分脚本语言,都采用传统的所谓的垃圾回收机制调控资源,这个回收机制虽然已经改善很多,但是仍然存在一些问题,一个是垃圾回收过程的不可预测性;其次是编写程序过程必须很小心,否则可能会出现相互引用而造成内存永远无法释放;象数据链、数组之类的复合型数据,相关引用很多,会影响数据处理效率。Cx语言完全没有这些问题。
07. 支持DCL, MFC
Cx除了支持AutoCAD提供的窗口控制语言DCL外,更强力支持Microsoft Visual C/C++的MFC编程。用MFC设计窗口,会比DCL强大、漂亮得多。由于Cx脚本特性,建立独立小单元的测试非常方便,学习、测试MFC特性是一件相当让人振奋的事。
Cx内部连接大量MFC类(class),可以直接使用这些类,无缝地继承这些类,虚拟函数、操作符重载、仿函数、枚举值,等等,都可以继承过来。对于窗口类,Cx处理消息要比C/C++语言简洁得多,不需要使用映射消息的数据结构和相应的宏。对于不同的窗口消息,Cx会自动调用相应名称的方法处理消息。Cx本身的类定义,支持“简化型”的多重继承,一个类里可以同时定义具有不同参数个数的构造函数,析构函数功能是健全的,在对象失效时,会及时启动。下面先给个简单的例子:
Mx = new AcGeMatrix3d();
Mx(1,1) = 123;
++Mx(1,1);
这里的操作,隐式调用了 AcGeMatrix3d仿函数。
Cx中定义的类,如果继承自MFC的CObject之类的类,可以拥有MFC类CRuntimeClass,进行相当底层的操作,比如进行动态产生窗口,象下面的例子,将主窗口分割成3个子窗口,利用CRuntimeClass为每个子窗口动态产生Cview型窗口:
//
#package myDemo
//
#include "windows.ch"
#define MY_PHONE 88
#define MY_EXIT 99
//
public function [C:C12]()
{
myC = new CCreateContext;
MyF = new CmyForm;
res = MyF.Create( NULL, "myDemo", WS_OVERLAPPEDWINDOW,
CFrameWnd::rectDefault, AfxGetMainWnd(),
NULL, 0, myC);
if (!res) return;
MyF.ShowWindow(SW_NORMAL);
MyF.UpdateWindow();
MyF.RunModalLoop();
}
//
class CmyForm : CFrameWnd
{
def OnCreateClient (lpcs, pContext);
def OnSize (type, cx, cy);
def OnClose ();
def OnNcDestroy () { EndModalLoop(1); }
CSplitterWnd m_sV, m_sH;
RECT m_rec;
int m_bCreateSplitter=FALSE;
};
//
function CmyForm::OnCreateClient(pcs, pcc)
{
pcc.m_pCurrentFrame = this;
siz = new SIZE;
rView1 = RUNTIME_CLASS(CmyView_1);
rView2 = RUNTIME_CLASS(CmyView_2);
rView3 = RUNTIME_CLASS(CmyView_3);
m_sV.CreateStatic(this, 1, 2);
m_sH.CreateStatic(m_sV, 2, 1, WS_CHILD | WS_VISIBLE,
m_sV.IdFromRowCol(0, 1));
siz.cx = 700;
siz.cy = 600;
if (!m_sV.CreateView(0, 0, rView1, siz, pcc)) return 0;
siz.cx = 200;
siz.cy = 200;
if (!m_sH.CreateView(0, 0, rView2, siz, pcc)) return 0;
if (!m_sH.CreateView(1, 0, rView3, siz, pcc)) return 0;
m_sV.SetRowInfo (0, 100, 10);
m_sV.SetActivePane(0, 0, NULL);
m_bCreateSplitter = TRUE;
SetWindowPos(0, 60, 40, 900, 600, 0);
ShowWindow(SW_NORMAL);
return 1;
}
//
function CmyForm::OnSize(type, cx, cy)
{
if (!m_bCreateSplitter) return;
GetClientRect(m_rec);
Width = m_rec.Right - m_rec.Left;
Height = m_rec.Bottom - m_rec.Top;
m_sV.SetColumnInfo(0, Width * 6/9, 0);
m_sV.SetColumnInfo(1, Width * 2/9, 0);
m_sH.SetRowInfo(0, Height /2, 0);
m_sH.SetRowInfo(1, Height /2, 0);
m_sV.RecalcLayout();
m_sH.RecalcLayout();
}
//
function CmyForm::OnClose()
{
if (MessageBox("Are you sure to quit?", "Quit", MB_YESNO) != IDYES)
return 1;
}
//
class CmyView_1 : CView
{
CPicture m_pic;
RECT m_rec;
def OnCreate(cs) {
m_pic.Load("highway.jpg");
}
def OnDraw (pDC) {
GetClientRect(m_rec);
m_pic.Render(pDC, NULL, m_rec);
}
};
//
class CmyView_2 : CView
{
CPicture m_pic;
RECT m_rec;
def OnCreate(cs) {
m_pic.Load("warShip.jpg");
}
def OnDraw (pDC) {
GetClientRect(m_rec);
m_pic.Render(pDC, m_rec);
}
};
//
class CmyView_3 : CView
{
def OnCreate (cs);
def OnCommand (msg);
def OnDraw (pDC);
CPicture m_picPhone1;
CPicture m_picPhone2;
CPicture m_picExtit;
CBitmapButton m_btnPhone;
CBitmapButton m_btnExit;
HWND m_hFrame;
};
//
function CmyView_3::OnCreate( cs)
{
rec = new RECT;
m_hFrame = ((CCreateContext) cs.lpCreateParams).m_pCurrentFrame->m_hWnd;
m_picPhone1.Load("phone_up.jpg");
m_picPhone2.Load("phone_down.jpg");
m_picExit. Load("exit.jpg");
sty = WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_OWNERDRAW;
rec.left = 30;
rec.right = rec.left+20;
rec.top = 50;
rec.bottom = rec.top +20;
m_btnPhone.AttachBitmaps(
(HBITMAP) m_picPhone1.GetHandle(),
(HBITMAP) m_picPhone2.GetHandle()
);
m_btnPhone.Create("", sty, rec, this, MY_PHONE);
m_btnPhone.SizeToContent();
sty = WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_OWNERDRAW;
rec.left = 150;
rec.right = rec.left+20;
rec.top = 50;
rec.bottom = rec.top +20;
m_btnExit.AttachBitmaps(
(HBITMAP) m_picExit.GetHandle()
);
m_btnExit.Create("", sty, rec, this, MY_EXIT);
m_btnExit.SizeToContent();
}
//
function CmyView_3::OnCommand(msg)
{
switch(msg.wParam) {
case MY_PHONE:
CreateDispatch("SAPI.SpVoice").Speak("Hello");
return 1;
case MY_EXIT:
::SendMessage(m_hFrame, WM_CLOSE, 0, 0);
return 1;
}
}
//
function CmyView_3::OnDraw(pDC)
{
s1 = "Hello";
s2 = "Exit";
pDC.TextOut(50, 120, s1, s1.len);
pDC.TextOut(170, 120, s2, s2.len);
}
//
#package _end_
//
08. 支持ForEach, map
作为脚本语言既酷、又富有效率的程序结构:ForEach 和 map,Cx强力支持。对于ForEach结构,Cx的表达方式有:
1) for (x in express [, len]) {…}
2) for (p to express [, len]) {…}
3) repeat(x in num [,Zero [, Step]]) {…}
其中express, 可以是各种形式的数组结构、数据链、字符串、和CAD选择集,等等。这类表达式已经不断出现在上面例子程序中。它们不仅表达简洁,而且可以让Cx程序的运行更富有效率,For(x in experss) 中的 x, 不仅对应各个数据结点的值,很多时候是这些数据的参照(Reference),让这些结点数据直接进行存取,比如,有数组:
A = new int[1000];
for (x in A) x = $i;
这里的 $i为Cx的系统变量,自动记录了该 for 循环的序号。因此,现在A中各元素的值分别是:
0, 1, 2, 3, 4, …, 998,999。
for (p to express) {…}结构,用于操作复杂的数据结点,比如ADS的resbuf数据链,p是指向各个数据结点的高级指针。通过该指针,可以方便操作所指数据结点的各种值。比如,建立如下resbuf的LIST数据链:
L = new LIST(<1,"Sa">, <2, "Sb">, <3, "Sc">);
得到LIST数据链:
((0 “Sa”) (1 “Sb”) (2 “Sc”))
进行 to 的 forEach操作:
for (p to L) {
p[1].value += "-x" + $i; //$i为Cx的系统变量,自动记录了 for 循环的序号
}
LIST数据链L的内容变为:
((0 "Sa-x0") (1 "Sb-x1") (2 "Sc-x2"))
对于Map的结构,Cx的表达方式有:
1) map_in(L1, L2, …) {
functionPtr;
}
2) map_to(L1, L2, …) {
functionPtr;
}
其中L1, L2, …可以为各种形式的数组结构、数据链、字符串、和CAD选择集等, FunctionPtr为Lambda、函数指针、或闭包(下篇博文有专门论述)。比如有3个数组:
#define MyLEN 1000
A = new double[MyLEN];
B = new double[MyLEN];
C = new double[MyLEN];
。。。初始化A和B各元素的值后,
map_in(A, B, C) {
Lambda(x, y, z) {
z = x + y;
}
}
完全等效C/C++中的表达式:
for (int i=0; i < MyLEN; i++) {
Z[i] = A[i] + B[i];
}
对于map_to,与 for(p to…) 的情况一样,用于操作复杂的数据结点。
09. 良好容错、除错能力
作为脚本语言,良好的容错性是很重要的,不像编译型程序语言的程序错误可以在编译过程发现和更正,脚本语言的很多错误只能在运行过程发现。因此,脚本语言应该有一定的纠错、“容错”能力。比如在前面讲到的程序在异常情况下中断,必须能够自动对要求成对操作进行善后处理。
Cx中的对象变量或对象结点,一般都具有双重性:指针和参照,因此,Cx对方法的调用,一般即可以用点操作符 “.”,也可以用指针操作 “->”。
Cx的标识符不区分大小写。函数名称、变量名称、类型名称等可以同名,因此不用担心变量名称的随意使用而可能导致同名函数、类型的失效。
采用下面程序结构,可以处理程序中可能的出错:
1)try {};
{}中出错时,跳出{},继续执行下去。
2)try {1} except {2};
{1}中出错时,跳到{2}中运行,如果没有出错,则跳过{2}。无论如何,程序都会在 {2}后继续运行下去。
3)try {1} finally{2};
无论{1}中是否出错,都会进入{2}中运行,然后程序都会在{2}后继续运行下去。
010. 支持(自)覆盖 / 热替换
Cx的函数定义,类定义,结构定义等,都可以进行(自)覆盖/ 热替换,程序运行过程可以随时随地装载新的程序,无论新的程序中的各种定义是否与旧的定义重叠,都没有关系,新、旧定义可以并存,旧的定义体仍然可以正常运作下去,新的定义也可以开始新的操作。即使在多线程复杂情况下也可以进行正常装载!
AutoLISP支持函数自覆盖特性,这是非常有用的特性,利用该特性,数以千计的功能、函数,可以在真正需要的时候才自动、随时随地装载,而不是在宿主程序启动时一次性装载而严重影响启动时间、也造成内存浪费。
用脚本语言进行大型软件的二次开发,说简单很简单,可能仅仅一行代码就可以完成一项任务,它的简洁、它的灵活性,是编译型语言不能匹敌的!但是二次开发、尤其脚本语言进行二次开发,说困难也困难,因为运行环境很恶劣,很难预料、不好控制,操作者的操作可能很随机,甚至是恶意的,如何去适应?二次程序开发者可能不止一个,素不相识的开发者的程序之间如何协调、共处而不冲突?二次开发的程序可能非常多,而可能调用的功能很随机,在不能把所有功能程序装载、甚至绝大部分功能程序都没有装载的情况下,操作者如何随心所欲、让宿主平台自动、简易调用需要的功能?程序重载、覆盖,是否会影响正在运行的程序? 等等,所有这些,让人眼花缭乱。
Cx语言的产生,最初的宗旨就是为了解决上面这些问题。很庆幸、也很意外的是,上面的这些问题,现在在Cx语言里已经不成问题了。
011. 支持正则表达式
例如下面是一个定义有效日期格式的正则表达式(考虑了润年影响,摘自网络文章):
exp = @"^(?:([0-9]{4}-(?:(?:0?[1,3-9]|1[0-2])-(?:29|30)|"
@"((?:0?[13578]|1[02])-31)))|([0-9]{4}-(?:0?[1-9]|"
@"1[0-2])-(?:0?[1-9]|1\d|2[0-8]))|"
@"(((?:(\d\d(?:0[48]|[2468][048]|"
@"[13579][26]))|(?:0[48]00|[2468][048]00|"
@"[13579][26]00))-0?2-29)))$";
rx = new regex(exp);
rx.match("2000-2-29"); // 返回 1 (有效日期)
rx.match("2001-2-29"); // 返回 0 (无效日期)
正则表达式是一种很特殊的语言,看起来怪异,不容易书写和上手,但是很有用,可以在网上找到不少实例。
012. 支持多线程编程、并行计算
作为脚本语言,Cx没有象Python那样的“全局锁”(GIL),所有运行于各线程的Cx程序,都是真正的系统级线程程序,程序语句独立、安全。一般脚本语言的内部实现,需要牵涉太多公共数据读和写,在多线程状态下要安全实现数据共享,往往需要付出太多性能代价,Cx非常成功解决了这个问题,代价很小,很安全,甚至在极端情况下,比如10个线程同时在同步锁协调下为某个整数增值操作,同步协调造成的运行速度损耗不会超过3%!如果采用传统同步锁方法进行同步操作,运行速度损耗可能高达85%!
下面给个比较测试的例子。Cx提供一种同步锁:单写多读同步锁wrLock,允许同时很多读操作,而写操作具有排他性,一次只允许一个写操作。比较用的同步锁程序来自 Jeffrey Richter编写的类CSWMEG,也是单写、多读同步锁。为了便于比较,该类已经被Cx关联,可以直接在Cx里调用。这里先给出结果,再列出相应的程序。程序的结构以Cx支持的并行计算方式书写,对数据成员m_res共做 1千万次增1操作。其中类CmyPa_1采用 wrLock同步锁,而类CmyPa_2采用 CSWMRG同步锁。函数C:13和C:14都调用类CmyPa_1,分别使用单线程和10个多线程操作,函数C:15和C:16都调用类CmyPa_2,方别使用单线程和10个多线程操作,结果如下(消耗时间:ms 毫秒):
同步锁 单线程 多线程 比较(单线程/多线程)
-----------------------------------------------------------------------
wrLock (Cx) 5,344 5,484 97.45%
CSWMRG (By Jeffrey Richter) 5,750 39,015 14.74%
-----------------------------------------------------------------------
从中可以看出极大的差异!显示Cx语言提供的同步锁无比优越的性能!需要指出的是,两种同步锁下的程序运行过程,CPU占用率都不太高,在20~30%之间徘徊。
//
#package myDemo
//
public function [C:C13]() // 用wrLock, 单线程
{
t1 = GetCurrentTime();
P1 = new CmyPa_1(1);
P1.start();
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
prinn("result = " + P1.m_res);
}
//
public function [C:C14]() //用WrLock, 10个线程
{
t1 = GetCurrentTime();
P1 = new CmyPa_1(10);
P1.start();
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
prinn("result = " + P1.m_res);
}
//
public function [C:C15]() //用CSWMRG, 单线程
{
t1 = GetCurrentTime();
P2 = new CmyPa_2(1);
P2.start();
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
prinn("result = " + P2.m_res);
}
//
public function [C:C16]() //用CSWMRG, 多线程
{
t1 = GetCurrentTime();
P2 = new CmyPa_2(10);
P2.start();
printf("Time = %d (ms)\n", GetCurrentTime() - t1);
prinn("result = " + P2.m_res);
}
//
class CmyPa_1 : CParallel //用WrLock
{
CmyPa_1( nThread) {
CParallel::initial( nThread);
m_nThread = nThread;
m_lock = new wrLock();
m_res = 0;
}
~CmyPa_1(){}
def InvokeProcess() {
my_lock = m_lock.__image();
repeat(10000000 / m_nThread) {
my_lock.EnterWrite();
m_res++;
my_lock.LeaveWrite();
}
}
wrLock m_lock;
__int64 m_res;
int m_nThread;
};
//
class CmyPa_2 : CParallel //用CSWMRG
{
CmyPa_2( nThread) {
CParallel::initial( nThread);
m_nThread = nThread;
m_lock = new CSWMRG();
m_res = 0;
}
~CmyPa_2() {}
def InvokeProcess() {
my_lock = m_lock.__image();
repeat(10000000 / m_nThread) {
my_lock.WaitToWrite();
m_res++;
my_lock.Done();
}
}
CSWMRG m_lock;
__int64 m_res;
int m_nThread;
};
//
#package _end_
//
多核PC已经普及几年了,能够利用多核优势进行编程的程序语言不多,脚本语言就更少了。为了有效使用多核,要求程序能够支持真正的多线程(Multihread)!
有一些脚本语言声称支持多线程编程,但是它们即使能够运行于多核,却达不到提高程序运算效率的目的,为什么?象现在很流行的脚本程序语言Python和Ruby,其多线程的运作是通过全局锁(GIL)调控,在GIL控制下,多线程的运行是畸形的,在同个时间段,只允许一个线程取得GIL,处于运行状态,其他线程是处于休眠状态!可想而知,在GIL控制下,这些多线程合计的运算效率,与一个普通单线程情况下的运算效率不会有多大差别!因此,人们一直期望能够移除这个GIL。Python到版本3.1、Ruby到版本1.9都没有能够移除掉GIL,倒是基于微软。NET平台的 IronPython和IronRuby没有采用GIL,这是一大进步。
脚本语言的实现,一般内部存在大量需要共享的数据,虽然在多线程环境里数据共享方法很早就有,也健全,但是就是存在明显的效率问题,线程一多,问题更大,可能会象死机一样,这是个令人非常头痛的问题。有人抱怨CPU提供的“同步”机制太弱,Intel等硬件厂商把无法在硬件上再提升速度的问题,转嫁给软件业。不过,这也反映出,软件的发展跟不上硬件的发展。
Cx程序语言的成功,在一定程度上是受益于多线程同步技术的探索取得突破性进展!为了适应多线程需要,Cx语言的架构就是基于多线程。它支持系统级多线程,能同时运行于CPU多核,不仅支持MFC的工作者(Worker)线程,而且支持用户界面(UI)线程。
上面的例子是围绕同步锁激烈竞争情况,下面给个没有同步竞争的多线程平行计算性能比较测试结果,与Python、IronPython 对比,测试过程是求从 1到5千万的和。分别测试在单线程和有10个线程并行计算的结果如下(消耗时间单位: ms 毫秒):
单线程 10个线程 (单线程/10线程)的比值
------------------------------------------------------------------
Cx 1875 547 3.43
Python v3.1 4747 4470 1.06
IronPython v2.0 6735 4030 1.67
------------------------------------------------------------------
限于篇幅,省略源程序。可以看出,Python 在多线程下并行计算得到的效率提升很少,几乎可以忽略不计。在多线程下IronPython的并行运算效率有些提升,而效率提升很多的是Cx语言!Cx语言的运算效率与核数量几乎成线性关系,核利用率高达85.75% (3.43/4)。从IronPython的结果看,感觉其内部运行机制中还是存在类似GIL的东西,只是效果比真正存在GIL时改进一些。
AutoCAD的操作不支持多线程,但是在AutoCAD里仍然可以正常运行多线程,只要这些多线程不牵涉操作CAD。