IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Linux  >

GUI 应用程序移植,第 1 章:移植架构

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


肖 习攀 (xiaoxp@cn.ibm.com), 高级软件工程师, IBM
阎 小兵 (yanxb@cn.ibm.com), 高级开发经理, IBM
贾 迎乐 (jiayingl@cn.ibm.com), 高级软件工程师, IBM
龚 奕平 (gongyp@cn.ibm.com), 高级软件工程师, IBM

2007 年 10 月 18 日

API 模拟的移植方法,是为现有大量Windows程序提供一个统一的移植到Linux平台的方案。其核心是,设计和开发人员不需要深入到每一段需要移植的程序源码内部,而在Linux上提供一层Windows程序运行所需要API的实现。这样,从理论上讲,已有的Windows应用程序GUI逻辑接口不加任何修改,就能和这一模拟层提供的动态库一起编译链接,生成Linux格式的可执行文件,从而完成移植。

引言

《GUI应用程序移植——在Linux上模拟Windows API的方法》一书介绍一种把Windows GUI应用程序移植到Linux的方法——API模拟方法。书中介绍了这种方法的设计思想,以及具体的实现过程。同时,充分探讨了GUI应用程序移植所必然面对的基于不同操作系统的编程模型的差异,揭示了两个系统有关用户界面交互和图形输出基本逻辑结构的一些鲜为人知的特性。我们推出了此书的第 14 章供大家在线浏览。

更多推荐书籍请访问 developerWorks 图书频道





回页首


1.1 移植架构的设计

图书信息 书名:《GUI应用程序移植》
作者:肖习攀、阎小兵、贾迎乐、龚奕平 等著
出版社:电子工业出版社
出版日期:2007 年 03 月
ISBN:978-7-121-03832-7
购买: 中国互动出版网dearbook

推荐章节:

更多推荐书籍,请访问 developerWorks 图书频道
如果您对本书有任何的建议、意见或者问题,欢迎与本书作者和 IBM 技术专家 交流

如前言所述,API模拟的移植方法,是为现有大量Windows程序提供一个统一的移植到Linux平台的方案。其核心是,设计和开发人员不需要深入到每一段需要移植的程序源码内部,而在Linux上提供一层Windows程序运行所需要API的实现。这样,从理论上讲,已有的Windows应用程序GUI逻辑接口不加任何修改,就能和这一模拟层提供的动态库一起编译链接,生成Linux格式的可执行文件,从而完成移植。

API模拟的方法可以最大程度地避免修改应用程序本身的设计和编码,但由于操作系统本身的限制,这种方法并不能涵盖所有的功能,即有些Windows上的功能在Linux上是无法通过这种方式来实现的,如OLE,COM,DDE和系统注册表等内容。但幸运的是,这些功能一来较少被应用程序使用,二来也很容易使用别的方法来替换或者避免,所以这些问题都不在设计之列。

另外,Windows系统支持的"文件类型关联"功能,在移植的过程中,也采取完全本地化的方法,即使用GNOME桌面系统提供的文件类型关联来替代。对于256色显示设置的支持,系统托盘(TrayIcon)的使用,以及Alpha通道绘制(AlphaBlend函数),也不做阐述。

同样,由于系统本身的限制,某些高级特性,如系统剪贴板、拖放(Drag and Drop)等需要和别的应用程序交互数据的功能,其支持的数据格式,也保持和Linux系统一致。例如,在Linux上,Metafile(元文件)格式的图像不被其他应用程序支持,那么CF_ METAFILEPICT格式的剪贴板以及拖放功能都不被支持。

虽然API模拟的方法可以推广用以建立跨进程的API模拟环境,但本书所论述的模拟环境,只限制在同一进程内工作。

最后需要提及的是,并不是所有的Win32 API都需要实现。而是根据应用程序所需,通常几百个Windows GUI函数所构成的Linux动态库,就可以满足大多数应用程序,甚至非常复杂的图形用户接口程序的需要。





回页首


1.2 API模拟层的结构

Linux上API模拟层的结构是模拟Windows GUI API的结构而产生的,由两个子系统USER32和GDI32组成。为了达到好的代码可维护性,模拟层再将这两个子系统细分成若干互相独立的模块。如图1-1所示,它给出了整个模拟层的结构。


图1-1 模拟层结构
图1-1  模拟层结构

USER32和GDI32模块是Windows图形界面的基础。在Windows系统中,这两个模块的函数分别由Windows目录中system32子目录下的user32.dll和gdi32.dll提供。简单地说,USER32模块主要负责窗口系统,如窗口的创建、销毁、遍历、消息触发和默认响应等方面。而GDI32模块则负责绘制,开发者调用GDI32的函数来绘制窗口内容,如文本和图形输出。在本书中,GDI32也被称作GDI(Graphic Device Interface,图形设备接口)。

图中的箭头标明了各个模块之间的依赖关系。在Linux上,由USER32和GDI32组成的模拟层位于GDK/GTK/X之上。事实上,在整个模拟层中,只有处于底端的两个模块,USER32的"底层服务"和GDI32的"屏幕"模块,直接依赖于GDK/GTK/X。高级的模块,如USER32的系统对话框、标准控件和菜单等模块,以及GDI32的GDI对象和高级GDI函数等模块,都仅仅依赖于模拟层内部的底层模块。另外,GDI32子系统是比USER32子系统更底层的模块,所以只有USER32模块对于GDI32模块的依赖,GDI32并不依赖于USER32。

总之,模拟层的每个模块都互相不知道其他模块的内部实现细节,除了使用数量很少的几个自定义函数来调用其他模块的功能之外,模块之间只使用Windows API来调用其他模块的功能。





回页首


1.3 API模拟层的实现方法

大家知道,X窗口系统的C语言编程接口被称为Xlib。而在Linux平台,GTK(The GIMP Toolkit)为GUI应用程序的开发提供了一个比Xlib更加友好的编程接口。和Windows GUI 编程模型相比,GTK属于高级的图形开发工具包。事实上,GTK的Windows版本就是基于Windows GUI API开发出来的。Windows提供了图形界面开发的基本概念和方法,GTK在此基础上建立起更加高级的概念和复杂的控件。所以,如果要在Linux上模拟Windows的行为,则必须再把基础建立在更加低级的平台上。基本上,模拟层主要只调用GDK的函数,同时也直接调用X的函数。GDK是GTK的一个模块,对X的函数进行了简单的封装。只有在某些较高级的行为上,如拖放(Drag and Drop)和系统对话框(Common dialog),模拟层才使用一点GTK的功能。

另外,在实现过程中,Windows和Linux/X平台之间的差异是一个无时无刻不面临的和十分棘手的问题。所以对于与Windows模型相同或者类似的地方,本书将简单带过,而着重于描述平台之间的差异,以及可以用来弥补这些差异的技巧和方法。

X窗口系统,以及建立在X窗口系统之上的GDK/GTK系统,是Linux上实现模拟层的支撑和手段。本节将对这些重要概念及其基本特性进行回顾。如果读者已经对X窗口系统编程有一定的经验,则可以跳过本节。

1.3.1 X窗口系统

X窗口系统为当前流行的多数Linux操作系统提供图形用户界面。在X窗口系统中,输入设备(键盘和鼠标)和输出设备(屏幕)只能由一个被称为X服务器的进程直接控制。其他需要显示图形输出和接收用户输入的应用程序,都必须连接到X服务器,向X服务器提交绘制屏幕的请求,并接收由X服务器分发的鼠标和键盘输入。虽然如此,开发者并不需要管理连接和通信的细节。如同使用Windows API一样,通过X窗口系统提供的Xlib接口,开发者可以不知道这个服务器/客户机结构的存在。Xlib接口的每个函数所做的事情,不过是将用户输入的参数打包,发送到X服务器;如果有返回值的话,还需要等待服务器的回应。观察"创建窗口"的Xlib函数,可以发现它和Windows提供的CreateWindow函数看起来没有多少差别。下面两段程序说明了这些差别。


	
Window XCreateWindow(display, parent, x, y, width, height, border_width, depth,
Class, visual, valuemask, attributes)
	Display *display;
	Window parent;//父窗口
	int x, y;//位置
	unsigned int width, height;//大小
	unsigned int border_width;//边框宽度
	int depth;//颜色深度
	unsigned int Class;
	Visual *visual;
	unsigned long valuemask;
	XSetWindowAttributes *attributes;
	

而CreateWindow函数的声明是:



HWND CreateWindow(LPCTSTR lpClassName,//类名
    LPCTSTR lpWindowName,//标题
    DWORD dwStyle,//风格
    int x,
    int y,//位置
    int nWidth,
    int nHeight,//大小
    HWND hWndParent,//父窗口
    HMENU hMenu,//菜单
    HINSTANCE hInstance,
    LPVOID lpParam
);

1.3.2 Display和X的服务器/客户机结构

Display是X上图形界面程序需要接触的第一个概念。Display被定义成由一个键盘、一个鼠标,以及一个或多个显示屏幕组成的一个工作台。从程序开发者的角度出发,Display是需要创建的第一个对象。X是一个天生的服务器/客户机结构。所以,一个X应用程序可以运行在一台机器上,而(键盘、鼠标)输入和(屏幕)输出在另一台机器上。运行X应用程序的进程,被称作X客户端,而管理输入和输出,即管理Display的进程,被称作X服务器。当用户在一台机器上运行X程序时,X服务器和客户端都在同一台机器上。X服务器需要负责管理以下内容。

  • 多个客户端程序对Display(鼠标、键盘和屏幕)的访问;
  • 理解并应答从网络传来的每个客户端程序的请求;
  • 将用户(键盘、鼠标)输入传送给客户端程序;
  • 显示客户端程序请求的(屏幕)输出。
  • 管理所有客户端程序创建的图形对象(窗口、off-screen位图、字体、光标等)。

X的服务器/客户机结构对于程序开发者和使用者而言都是透明的。对于开发人员,并不需要将每一个输出请求显式地通过网络API发送到远端(或者本机)的X服务器。和在Windows上的开发一样,用户只需要调用X的显示输出函数,就可以得到屏幕显示结果。但需要注意的是,X的显示输出函数,仅仅是将输入参数涉及的数据打包,并且放到一个缓冲区中。在合适的时候,该缓冲区中的数据被成批地发送到X服务器。X服务器在接到这样的显示输出请求之后,将更新自己维护的内部数据结构,并刷新硬件输出,从而将用户希望显示的结果输出到屏幕上。由上述可知,客户端程序不能假设在函数调用结束时,实际的输出操作已经产生。

下面是一个最简单的X程序的例子。



#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
int main( int argc, char *argv[] )
{
   Window win;//主窗口
   Display *display;//Display

   //从环境变量取得X Server地址,如果没有设置,NULL值默认为本机的X Server 
   //X Server地址为 192.168.0.123:0.0 的格式
   char *display_name = getenv( "DISPLAY" );

   //连接X服务器。从开发者的角度来看,Display更像是一个到X服务器的网络连接,一个socket
   if ( (display=XOpenDisplay(display_name)) == NULL )
	   return -1;

   //创建窗口
   win = XCreateWindow(display, XDefaultRootWindow(display),
         10, 10, 500, 300, 1, CopyFromParent, 
		 CopyFromParent, CopyFromParent, 0, NULL );

   //注册窗口感兴趣的事件
   XSelectInput(display, win,  KeyPressMask | ButtonPressMask );
   //显示窗口
   XMapWindow(display, win);

   //进入主循环,等待用户输入
   while (1)
   {
      XEvent msg;
      XNextEvent(display, &msg);
      switch  (msg.type)
	  {
	  //当有键盘或者鼠标点击输入时,简单退出程序,作为响应
      case ButtonPress:
      case KeyPress:
         XCloseDisplay(display);
         exit(1);
      default:
         break;
      } /* End switch */
   } /* End while */
   return 0;
}

值得注意的是,客户端程序只能通过听取服务器发送过来的事件,来获得准确的图形对象数据。下面是Windows程序代码片段。



RECT rect = { 10, 10, 500, 300 };
BOOL bMoved = MoveWindow( hWnd, rect.left, rect.top, 
   rect.right-rect.left, rect.bottom-rect.top, TRUE );
if( bMoved )
{
   RECT rectWindow;
   GetWindowRect( hWnd, &rectWindow );
  assert( 
	  rect.left == rectWindow.left &&
	  rect.top == rectWindow.top &&
	  rect.right == rectWindow.right &&
	  rect.bottom == rectWindow.bottom );
}

Windows运行时,上段程序会先用MoveWindow函数试图将窗口hWnd移动到一个特定的位置,然后用GetWindowRect函数查询窗口当前的位置,查询结果应该和用户请求的位置一致。但是在X中,情况并非如此。下面是与之类似的X程序片段。



XMoveResizeWindow( display, window, 10, 10, 500, 300 );
XWindowAttributes attr;
XGetWindowAttributes( display, window, &attr );

attr结构中将会返回该X窗口的位置和大小信息的数值。一般地,这个值和用户指定的值并不相等。为了保证从X服务器得到图形元素的正确数据,用户必须监听一定的X事件。服务器在处理某些用户输出请求、更新内部数据之后,会以X事件的形式将新的值通知客户端程序。在下一小节中读者将会看到,由于窗口管理器的影响,用户根本不能假设改变窗口大小的请求一定会被完全地处理。窗口管理器可能会将该窗口移动到一个根本不是用户指定的矩形中去。

1.3.3 窗口管理器

X的另一个重要特性是,应用程序本身并不能完全控制窗口的大小和位置。因为各种应用共享一个显示屏幕,一个应用程序不能总靠一种窗口布局来显示自己的内容。应用程序只能向系统建议自己的窗口是什么样子的。窗口管理器(Window Manager)就是统一管理窗口布局和其他一些窗口行为的进程。窗口可以向窗口管理器建议自己的位置和大小,但是这些建议不一定能被采纳。

例如,如果一个窗口管理器强制要求所有的窗口都不能互相重叠。那么,当应用程序建议某一个位置和大小时,窗口管理器可能将该窗口显示到一个没有别的窗口的地方。所以在X上,应用程序应该做好应对各种可能的窗口管理器的准备。 除了管理窗口的位置和大小之外,窗口管理器还管理诸如窗口标题条、窗口边框、窗口键盘输入模式等。例如,在某些窗口管理器中,只有鼠标指针所在的窗口能够接收键盘输入。

1.3.4 X事件

和Windows及其他的GUI驱动的应用程序一样,X程序必须等待和接受用户输入及其他系统消息,来响应用户的操作。下面的两段代码分别列出了Windows和X的GUI界面程序的主循环。



//Windows主消息循环
 MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) 
{
	TranslateMessage(&msg);
	DispatchMessage(&msg);
}

//X主消息循环
while (1)
{
   XEvent msg;
   XNextEvent(display, &msg);
   switch  (msg.type)
  {
  //当有键盘或者鼠标点击输入时,简单退出程序,作为响应
   case ButtonPress:
   case KeyPress:
      XCloseDisplay(display);
      exit(1);
   case Expose: //处理重绘消息,更新屏幕输出
      //…
   default:
      break;
   } /* End switch */
} /* End while */

和Windows消息不同,X只提供少数的几种基本事件,包括用户输入事件和其他程序交互时产生的事件。从这一点上讲,X事件比Windows消息要原始得多。X Release 6.3 的各种事件类型见表1-1。


表1-1 X R6.3的事件
表1-1  X R6.3的事件

1.3.5 GDK和GTK

如前所述,GDK对Xlib进行了封装;而GTK则建立起自己的消息队列和事件模型。在GTK中,事件不再是通过一个大的switch/case语句分发(像Windows和X程序那样),而是采用了信号/回调机制。信号和消息类似,即当某些事情发生时,用户程序会得到一个通知。而通知的传达形式,就是回调函数。为了给读者一个直观的印象,下面是一个最简单的GTK程序。



#include <gtk/gtk.h>
int main( int argc, char *argv[] )
{
	GtkWidget *window;
	gtk_init( &argc, &argv );//初始化GTK

	//创建一个顶级窗口
	window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
	gtk_widget_show( window );//显示窗口

	//加一个信号处理函数,使得当用户试图关闭该窗口时(比如,点击右上角的X型Close按钮),
	gtk_main_quit函数被调用,从而gtk_main函数将返回
	g_signal_connect( G_OBJECT(window), "delete-event", 
		G_CALLBACK( gtk_main_quit ), NULL );

	//进入主循环,等待用户输入
	gtk_main();

	return 0;
}

从上面的代码片段可以看出,一般的GTK程序并没有一个诸如GetMessage/ DispatchMessage的主消息循环,取而代之的是gtk_main函数。GTK使用gtk_main函数,阻止了应用程序直接访问主消息循环,而只能够通过g_signal_connect函数来注册一个回调函数作为信号处理器。在本例中,程序注册了名为"delete-event"的一个信号,表示用户试图关闭该窗口的事件,并使用gtk_main_quit函数作为回调函数。这样,当用户试图关闭该窗口时,gtk_main_quit被调用,从而中断执行在gtk_main函数内部的GTK主消息循环,使应用程序退出。如果删除g_signal_connect函数调用,则当用户试图关闭窗口时,该窗口确实被关闭,但是进程并没有退出。

实际上,gtk_main函数也可以被拆分成几个小的函数,从而使应用程序可以运行像Windows程序那样的主消息循环,达到控制主循环的目的。





回页首


1.4 编译器差异

开始移植工作后的第一步就是在目标平台Linux上进行编译,并链接源代码。由于需要移植的软件通常并未在Linux平台上编译过,编译的过程可能会遇到很大的困难。一般情况下,由类型声明引起的编译错误是比较容易修复的。比如Microsoft C/C++的头文件使用__declspec( dllimport/dllexport )来输入和输出DLL函数,在Linux上,把函数声明成extern "C",或者再结合使用DEF文件,使用相应的链接命令就可以解决这些问题。但困难的地方在于编译器之间存在差异的部分,同时这也是可能引起很多运行时问题的重要因素,读者有必要在开始移植之前就充分了解。本节不能概括所有差别,只讲述一些容易被忽略而且后果比较严重的方面。

以Visual C++ 2003和GCC 4.1.0为例。前者是Windows平台的主流编译器,兼容性良好,但是对C++标准的遵循并不严格。这意味着即使开发者写出不太符合标准的程序,编译器也可能能容忍。相反的是,GCC对标准的遵循相对严格得多,这样很容易造成在Windows运行良好的程序,在Linux上却引起意想不到的编译甚至运行时错误。

(1)基本类型大小和结构对齐

首先是C/C++语言基本类型的大小,以及相应的结构对齐问题。典型的例子是long关键字。在Visual C++ 2003下,sizeof(long double)是8,其大小和double一致。但是在GCC 4.1.0上,sizeof(long double)等于12,其大小比double多4。另一个和大小相关的问题是对齐问题。不同编译器的默认对齐大小是不一样的。一般情况下程序逻辑都跟对齐无关,但是涉及从磁盘或者网络文件中读取结构时(如解析资源),精确的对齐就是必需的。考察下面的程序段:



#include <stdio.h>

struct A
{
        char a;
        double b;
};


int main()
{
	printf("%d %d %d\n", sizeof(long double), 
        sizeof(long long), sizeof(A) );
	return 0;
}

上面这段程序在Visual C++ 2003编译器默认设置下,输出结果为8 8 16;在GCC 4.1.0编译器默认设置下,其输出为12 8 12。从sizeof(A)的大小可以看出,Visual C++ 2003是按8字节对齐的,而gcc是4字节对齐的。这时需要使用#pragma pack预编译指令来修改头文件中的结构声明,或者在运行时调整内存中结构成员的位置。不管采用何种方法,对齐都是需要小心处理的事情。

一个引起最大麻烦的基本类型是wchar_t。在Visual C/C++ 2003编译器中,wchar_t的大小是2字节,并且可以和unsigned short类型互相赋值。与此关联的一系列Unicode相关函数,比如wcslen,wcscmp等,都接受UTF16格式的Unicode串。在GCC中,其大小是32位。与此相关的wcslen,wcscmp函数都接受UTF32格式的Unicode串。为此,必须在Linux上开发一套UTF16接口的wcs系列函数,以保证UTF16的字符串被正确处理。与此同时,使用宏定义来替换wchar_t关键字为unsigned short,以保证函数声明的兼容。

(2)new操作符的出错处理

另一个问题是new操作符的出错处理。由于编译器的设置不同,new操作符可能具有不同的行为。考察如下的代码段:



#include <stdio.h>

class A
{
public:
	void *operator new( size_t size )
	{
		return NULL;
	}
	A()
	{
		printf("Constructor called\n");
		a = 0;
	}
private:
	int a;
};

int main()
{
	A *p = new A();
	printf("%x\n", p );
	return 0;
}

在Visual C++ 2003中,上面的程序输出0。而GCC 4.1.0编译器的输出结果为:


Constructor called
Segmentation fault

也就是说,Visual C++ 2003的编译器会检查new的返回值,如果返回为空,构造函数就不再执行。但是gcc必须加上-fcheck-new编译参数才具有这一行为:g++ -fcheck-new test.cpp。这样在Linux上上述程序也会输出0。

(3)结构化异常和C++异常

还有一个更隐蔽的差异存在于异常处理。Visual C++并不遵循异常处理的C++规范。考察如下的程序段:



#include <stdio.h>
int main()
{
    int* p = NULL;
    try
    {
        *p = 0;
    }
    catch (...)
    {
        printf("caught the exception\n");
        return 1;
    }
    return 0;
}

读者可以自己用Visual C++ 2003和GCC分别检验这段程序。前者生成的程序在Windows上正常运行,输出caught the exception,然后正常退出。而GCC生成的程序只是输出Segmentation fault。所以在Windows上,catch语句抓住了一个异常。按照C++的标准,只有使用throw语句,才能产生异常。但是在上面的程序段中,只是一个简单的赋值语句。原因在于,Visual C++ 2003将C++的异常处理映射成了Windows的结构化异常处理。在上面的语句中,*p = 0将引起一个Windows的异常,Visual C++将它处理成一个C++异常,并进入catch块。在Linux上,由于没有C++异常发生,程序直接崩溃。

上述给出了一些典型的编译器差别,其他细微差别未能全部总结并在此罗列,读者在实际开发过程中,需尽早意识到这些问题,以避免不必要的调试努力。





回页首


1.5 示例程序介绍

作为示范,本书提供了一个典型的"Hello World"Windows程序,及其使用的Windows API在Linux上的模拟实现,如图1-2所示。使用这一框架,读者可以自行将本书后续章节中示范的代码片段加入本例的程序中。为简单和缩小代码篇幅,例子中涉及的Windows API的模拟实现只是本书将要描述的实现的极简化版本。读者可以根据本书描述的方法,自己将这些API的实现补齐。


图1-2 "Hello World" Windows程序在Linux上的运行结果
图1-2  Hello World Windows程序在Linux上的运行结果

本例的所有代码,详见附录。





回页首


1.6 小结

本章主要介绍了模拟层的结构设计和实现要点。在下面的章节中将深入探讨组成API模拟层的内部,介绍GDI32和USER32子系统中的各个重要功能模块及其在Linux上的模拟实现。



作者简介

肖习攀,清华大学计算机科学与技术系硕士。IBM中国开发中心高级软件工程师。从2003年4月加入IBM中国开发中心至今,一直从事旨在提高企业生产效率的应用软件开发。多个项目开发组长。经历涉及Windows、Linux和Macintosh等平台,兴趣包括图形用户界面移植和企业协作式应用软件开发。


阎小兵,在中国科学院计算技术研究所获得硕士。1999年加入IBM中国公司。现任IBM中国开发中心高级开发经理。曾参与IBM CICS移植,IBM WebSphere Commerce移植,IBM Productivity Tools开发与项目管理等。目前主要工作领域是下一代企业级工作场所协作软件的开发。对跨平台(Windows, Linux, Macintosh)软件开发和移植,以及大型软件开发过程管理和项目管理有着丰富的经验。


贾迎乐,北京邮电大学信息工程学院硕士。目前是IBM中国软件开发中心高级软件工程师,从事IBM产品在Linux平台上的开发和移植工作。在C/C++编程语言,Windows和Linux图形用户界面应用程序的开发和移植等方面有深入的研究。


龚奕平,清华大学计算机科学与技术系学士,多伦多大学计算机科学系硕士。IBM中国开发中心高级软件工程师。现主要从事IBM产品的研究和开发工作。研究兴趣包括Windows应用程序跨平台移植,GUI开发,以及网络设备开发等。曾在国内外期刊杂志上发表多篇学术和技术文章。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款