Archive for May, 2009

subversion_logo_hor-468x64

Win32平台下基于SVN开发的若干问题整理

16

 

Keywords: svn  svn_cmdline_init crash 1.5.6 win32

subversion_logo_hor-468x64

 SVN(SubVersion)是目前开源的版本管理工具中较为流行的,最新的release是1.6.1。但是遗憾的是基于SVN开发的资料在互联网上异常的少。

         最近一段时间,我在工作上需要编写一个使用SVN进行版本管理的编辑器。(需要对编辑器生成的文件做版本管理以利于多人合作开发)起初是通过调用svn自身携带的的客户端程序svn.exe以实现相关功能,然而由于实际中的若干设计问题,最终的速度很不理想。于是产生了借用svn自身提供的api接口实现所需功能的想法。

 

         在实际使用svn提供的api时遇到了一些问题,经过多番查找资料以及尝试最终得以解决。将问题整理在此,以方便日后与我遇到类似问题的朋友。

 

         Svn提供的第三方开发接口sdk可以在这里下载到。目前最新的release是1.6.1。我在项目中实际使用的版本是1.5.6。开发需要的几个压缩包分别如下:

Svn发布的2进制可运行文件,包括客户端与服务端,以及所需的dll:

http://subversion.tigris.org/files/documents/15/45230/svn-win32-1.5.6.zip

Svn开发所需的lib及头文件以及文档:

http://subversion.tigris.org/files/documents/15/45236/svn-win32-1.5.6_dev.zip

Svn的调试符号文件:

http://subversion.tigris.org/files/documents/15/45234/svn-win32-1.5.6_pdb.zip

 

此外,在编写基于SVN的程序时,在链接时还有一些必要的库需要添加,分别是:

Berkeley DB for Windows:

http://subversion.tigris.org/files/documents/15/32472/db-4.4.20-win32.zip

libintl binaries for Windows:

http://subversion.tigris.org/files/documents/15/20739/svn-win32-libintl.zip

 

有了这几个包,就可以使用SVN提供的api开发基于SVN的第三方软件了。

 

         基于SVN1.5及之后的版本开发的时候,会遇到一个CRT冲突的问题,该问题会导致在调用svn_cmdline_init时,如果在error stream参数中传入非NULL值,则会直接crash。一个针对该问题的描述可以参见这里

         我下载了调试符号以及源代码之后,跟踪调试到svn_cmdline_init函数当中,发现crash发生在一个CRT函数(setvbuf)的调用当中。实际跟踪调试的过程中发现,虽然该函数是crt中的函数,然而vc调试器却无法基于本地的调试符号正确跟踪到其源文件当中。

又该问题在1.4及之前的版本不存在。究其原因,我猜测可能是由于自1.5版本之后,SVN提供的库是基于动态链接的CRT库的。而svn本身的lib在编译时链接的crt与客户的VC6的CRT版本不符导致的。(1.4版本之前则是静态链接的CRT)

对该问题的描述还可以参见这里

 

The problem is stderr — it is a FILE* which is a CRT type which means 
you have to be using the same CRT as the dll’s were built with. 
Unfortunately, there are a few API’s where raw CRT types crept in that 
aren’t wrapped by APR. 

You can either avoid those API’s, rebuild Subversion with your 
compiler, or link to the static libraries instead of the dynamic 
libraries and use the no-default-libs option to make sure that your 
CRT is linked in instead of the VC6 one (I believe this is how it 
worked prior to 1.5). 

DJ 

 

解决该问题的方法之一是自行编译svn的全部源码,而不使用官方提供的dev包。然而svn的源码编译要求的环境异常复杂(需要配置非常多的环境项:包括cygwin,python等等)。更简单的解决方案是使用静态链接版本的svn库。

svn_cmdline_init位于libsvn_subr-1.lib当中。该版本的lib在1.5及以后的版本开发包中是动态库,实际运行时需要libsvn_subr-1.dll的支持。同在该开发包中发布的还有一个静态版本的该库,名为svn_subr-1.lib。

但是链接静态版本的svn_subr-1.lib时,由于其中静态链接了CRT库,因此VC自带的libcmt.lib/libcmtd.lib会与之冲突。实际链接时会报出一堆同一个符号被多次链接的错误,只需将VC自带的libcmt.lib/libcmtd.lib设置忽略即可。

Ogre源码剖析3–可扩展性&插件机制

0

Ogre是一个跨操作系统平台的开源3D引擎,既支持DirectX,也支持使用OpenGL,支持可替换的场景管理算法(BSP, OCT)。为Ogre提供这些灵活可扩展性的基础之一就是其面向插件的设计。

 

很多常用的软件大都提供了插件接口,用以扩展应用程序设计者最初未想到的功能,比较常见的譬如PhotoShop的滤镜,After Effect中的各种插件(最有名的比如shine),3dMax的插件譬如渲染器,魔兽世界的辅助插件等等。

通常,插件本身通常也需要实现主应用程序所需要的必要接口,从而使得插件可以被应用程序加载执行。此外,插件的实现也需要由主应用程序提供一些接口api,通过这些接口,插件可以对主应用程序的功能进行调用。

插件可以是动态链接库(win32平台上为DLL文件),也可以是以脚本的形式提供的,比如魔兽世界中的插件就是使用lua编写的,插件也可能是某种应用程序自定义的文件,只要该应用程序提供了创建该类文件的方法并实现解析、执行功能即可。(不同的实现形式各有利弊,具体需要参考插件及应用程序所处的运行环境进行取舍)

采用插件的一个巨大的好处,以及很多应用程序中使用插件的主要目的就是,我们可以在不需要改动应用程序本身的情况下扩展应用程序的功能

 

在Ogre中,插件被用来提供渲染子系统(RenderSystem),不同类别的图形API被封装在不同的渲染子系统的视线当中,Ogre默认提供了DX和OpenGL的实现,甚至,如果我们乐意,甚至可以只用绘点函数实现一套纯软件的渲染子系统提供给Ogre使用。以此为例,用简单的形式来展示这种实现大概类似于下面这样:

class RenderSystem

{

         // … operations that a render system need to support

};

// In DX_RenderSystem.dll (in plugin dx rendersystem )

class DX_RenderSystem : public RenderSystem

{

         // … implementation & override of the operations from RenderSystem using DirectX

};

// In GL_RenderSystem.dll (in plugin openGL rendersystem)

class GL_RenderSystem : public RenderSystem

{

         // … implementation & override of the operations from RenderSystem using OpenGL

};

// …. We could implement any other rendersystem as we like

 

在引擎的内部(OgreMain),插件在初始化的时候,将一个渲染子系统的实例创建出来(可能是DX_RenderSystem,也可能是GL_RenderSystem,也可能是其他),并将之挂接到OgreMain的Root对象当中。此后,OgreMain中的Root所有的渲染操作也就可以通过该接口访问插件中创建的渲染子系统了。

 

         本文接下来分析Ogre是如何实现插件的,以及插件是如何与OgreMain主引擎进行配合的。

         事实上,想编写一个插件是很简单的,我们只需要约定几个接口,插件将这些接口实现了,主应用程序通过某种机制(配置文件/遍历某个目录)加载插件,并查找接口在插件中是否被实现了,如果实现了,则调用之即可。

 

一个简单的例子如下:

// in xx.dll

__declspec(dllexport) extern “C”    // __declspec(dllexport)告知编译器需要将该函数导出

                                                                 // extern “C” 告知编译器不要对函数做C++名字重整

const char* GetPluginName(void)

{

         return “Test Plugin”;

}

 

// in application

HMODULE hInst = LoadLibrary(“xx.dll”);

if (!hInst) return;

typedef const char* (*GetPluginNameFunc)(void);

GetPluginNameFunc pFunc = GetProcAddress(hInst, “GetPluginName”);

if (!pFunc) return;

const char* szPluginName = pFunc();

// … do other things

 

事实上,在C语言里,我们就可以通过这种方式,约定一套需要实现的接口,交给插件实现即可。在这里,插件的功能都是在dll中实现的,我们需要加载dll,保存句柄,并在插件卸载的时候释放相应的资源。在C++中,我们显然可以做的更好一点,比如将dll模块相关的功能以及资源封装起来:

 

class DynLib

{

public:

         bool Load(const char* szPluginPath);

         void* GetProcAddress(const char* szProcName);

private:

         HMODULE m_hInst;

};

 

bool DynLib::Load(const char* szPluginPath)

{

m_hInst = LoadLibrary(szPluginPath);

return m_hInst != NULL;

}

 

void* DynLib::GetProcAddress(const char* szProcName)

{

         return ::GetProcAddress(m_hInst, szProcName);

}

 

这样一来,我们就有了一个DLL的简单封装。

但是一个dll里未必只能有一个插件,假若我们简单的把插件接口设计为一套C导出函数,我们就不得不面对这种局限。经典的做法依旧是抽象,Ogre中定义了一个插件接口,每一个实现了插件接口的实例都代表了一个插件,如下:

 

class Plugin

{

public:

         virtual const String& getName() = 0;

         virtual void install() = 0;

         virtual void initialize() = 0;

         virtual void shutdown() = 0;

         virtual void uninstall() = 0;

};

 

Ogre的每一个插件都需要实现以上接口。该接口提供的功能包括:

install 插件被加载时调用。

initialize 插件被初始化时调用。

shutdown 插件被反初始化时调用。

uninstall 插件被卸载时调用。

 

不过,单有这个接口,插件还是无法工作。插件中还需要提供两个约定的C导出函数。这个稍后再说。

 

Ogre在Root对象中统一管理插件,因此Root对象提供了以下与插件相关的接口:

class Root

{

         // …

void loadPlugin(const String& pluginName);

void unloadPlugin(const String& pluginName);

void installPlugin(Plugin* plugin);

void uninstallPlugin(Plugin* plugin);

// 以及对应的处理多个插件的接口

         // …

};

 

当loadPlugin函数被调用时,Ogre将加载该插件的dll,从中查找并调用名为dllStartPlugin的导出函数。

对应的,当unloadPlugin函数被调用时,Ogre将从该dll中调用dllStopPlugin函数。并将该dll卸载。

 

每个插件在实现的时候,都必须提供上述的两个C导出函数。其中,dllStartPlugin负责创建插件实例,并调用Root::installPlugin,将创建好的插件指针传递给Root对象。在这里,dllStartPlugin实际可以创建多个插件实例,并依次调用Root::installPlugin,这样我们就可以在一个DLL中包含多个插件了。在dllStopPlugin时,则需要调用Root::uninstallPlugin,并将插件DLL中创建的plugin实例释放掉。

 

一个典型的dllStartPlugin的实现像这样:

 

class D3D9Plugin : public Plugin

{

         //…

};

 

D3D9Plugin* plugin;

__declspec(dllexport) extern “C”

void dllStrartPlugin(void)

{

         plugin = new D3D9Plugin();

         Root::getSingleton().installPlugin(plugin);

}

 

__declspec(dllexport) extern “C”

void dllStopPlugin(void)

{

Root::getSingleton().uninstallPlugin(plugin);

delete plugin;

}

 

Root::getSingleton().installPlugin(plugin)会负责调用插件的install以及initialise操作,并将插件的指针存放起来,以备卸载时使用。

 

Plugin::install以及Plugin::initialise则分别负责创建OgreMain提供扩展功能接口的实例,以及将创建好的对象挂载到应用程序当中。

 

一个典型的Plugin的行为像这样:

 

void BspSceneManagerPlugin::install()

{

         mBspFactory = new BspSceneManagerFactory();

}

 

void BspSceneManagerPlugin::initialise()

{

         Root::getSingleton().addSceneManagerFactory(mBspFactory);

}

 

其中,BspSceneManagerFactory是继承自OgreMain中的SceneManagerFactory的派生类。

( class BspSceneManagerFactory : public SceneManagerFactory {…} )

 

通过将场景管理器工厂添加到Root对象当中,插件动态的将其功能添加到了OgreMain当中。

 

PS: SceneManagerFactory是一个用于创建SceneManager的工厂。SceneManager则是被用于场景管理的管理器。具体的SceneManager也根据算法不同而不同,Ogre自带提供了两种类型的场景管理算法插件:Bsp以及Octree(实现了两个对应的插件,分别是Plugin_BSPSceneManager以及Plugin_OctreeSceneManager)。

 

通过上述方式,OgreMain核心引擎并不需要关心场景管理算法的具体实现,不需要关心渲染子系统的具体实现,不需要关心粒子系统的具体实现,等等。一切这些扩展性的功能都通过插件实现,并在加载时动态挂载到OgreMain当中,供OgreMain的引擎核心使用。

 

使用插件时几个需要注意的地方:

1,  调用插件DLL方法创建的对象需要交由插件DLL释放。(因为不同的链接单元可能具有不同的内存管理上下文环境,此处的new与彼处的new在语义上未必等同)

2,  调用插件DLL方法获取的插件内对象的引用或指针,在插件DLL卸载之后就是无效的,必须保证不再使用。(比较容易引发问题的一个典型例子是从插件中传递回一个引用计数字符串,当DLL被卸载后,字符串内指向实际数据的指针已经无效,但是在该对象析构时,仍需要访问该指针)

小议Command模式 抽象在实际编程中的应用

0

在许多编辑器中,大都实现了Undo / Redo操作。从界面出发,Undo/Redo仅仅只是一种抽象的”提供单一接口行为“的操作,尽管实际的Undo/Redo工作可能会涉及各种不同的具体操作。举个例子,在3dMax这样的软件中,我们可以创建3D实体,可以编辑其自身的各种属性(譬如立方体有长宽高,球有半径),可以移动实体的位置,对其做旋转,缩放操作,乃至对个别的网格顶点,面片的细微调节。其中的每一步,都可以撤销或重做。

我们仔细思考一下,就会明白,撤销和重做实际上是需要保存每一步操作的”被操作实体的两个状态“(分别是该实体在操作发生前和发生后的状态)。

譬如对一个物体做以移动,我们实际上是保存了移动前的位置,和移动后的位置,这样在Undo/Redo时,只需要分别将其置于所需的状态即可。

而不管具体的状态是如何转变的,对于用户来说,撤销/重做都只是一个简单的命令,表现为一个按钮,或者一次Ctrl+Z。这其实就是一种具有同一性表现的动作,在软件实现中,非常适合使用Command模式对其进行抽象。

 

譬如,我们只需要定义这样的一个基类即可明晰撤销/重做操作的概念接口:

class Command {

public:

         virtual ~ Command() = 0 {}

         virtual bool Do() = 0;

         virtual bool Undo() = 0;

bool Redo() { Do(); }

};

 

所有的Undo/Redo的状态,都派生自这个Command抽象类,它定义了两个纯虚函数,用于做和撤销。Redo函数只是简单的调用Do()实现其功能。

 

有了上面这个接口,当我们想添加更多类型操作的Undo/Redo支持的时候,只需要这样做:

class MoveBoxCommand{

public:

         MoveBoxCommand(VECTOR3 vNewPos, int boxid)

                   : m_iID(boxid), m_vNewPos(vNewPos) {}

         ~MoveBoxCommand() {}

bool Do()

{

m_vOldPos = SomeManager->GetObject(m_iID)->GetPos();

         return SomeManager->GetObject(m_iID)->SetPos(m_vNewPos);

}

bool Undo()

{

return SomeManager->GetObject(m_iID)->SetPos(m_vOldPos);

}

private:

int m_iID;

VECTOR3 m_vNewPos;

VECTOR3 m_vOldPos;

};

 

现在我们就可以对任意的对象支持Move操作的撤销/重做了,事实上,旋转,缩放等其他操作大都与此类似,对添加删除之类的操作需要做一点特别的处理(因为我们不能简单的把实际对象释放掉,否则其他引用了该对象的Command就会出现问题,一种良好的做法是设置删除标志)。

除此之外,我们需要将所有的操作产生的Command实例放在一个管理器中统一管理,假设其被命名为CommandManager。则CommandManager需要负责维护当前的Command,需要在用户调用Undo时,判断是否能够Undo,如果能够,则取出下一个Undo的Command,调用其Undo,并将当前Command的指针或者索引更新位置。

如果用户在某个点做出了新的操作,则需要将当前Command之后的所有Command清空。(如果用户没有做新操作,则可以使用当前Command之后的Command来实现Redo操作的,但是新操作会冲掉位于Redo序列中的元素)

如果撤销/重做的实现有了以上理解,就不难理解为什么几乎所有的软件在用户Undo多步,再采取新动作之后,就无法Redo了。

 

通过这种方式,我们就可以实现一个具有可扩展性的撤销重做机制了,不论再加入何种类型的新操作,我们都可以设法将其纳入到上述的框架当中。

 

具体的撤销重做实现还牵扯到许多细节,不过那都与主题关系不大,我们再来看一个例子:线程池。

 

有时我们可能会想编写一个线程池,用于处理各种并发的任务。线程池从概念上说是一个很简单的东西,有一个线程的集合,里面的很多线程都睡着,当有任务来的时候,一个线程跳出来把任务领走并执行,执行结束后,线程继续回到池子里睡觉。如果有多个任务,那么就会有许多个线程都跳出来,直到池子里没有更多的空闲线程或者任务队列被领空为止。

 

在Win32平台下编写多线程程序,大致会像这样:

void ThreadProc(PVOID param)

{

         // do something

}

 

// somewhere in some func, we create the thread

int handle = __beginthreadex(ThreadProc, valuePassin);

 

问题来了:

线程处理函数一般只有一个通用的模板,类似于上述的ThreadProc,线程池中的线程在创建的时候,需要传递一个处理函数地址,但是由于线程池中有许多线程,他们是批量被创建出来的,在创建这些线程的时候,我们无法立即获知所需处理的内容。因此,经济适用的方法是使用一个通用的线程处理函数,所有的线程被创建时传递的都是这个公共的处理函数。(而不是对每一个任务创建一个采用不同线程处理函数的新线程–这就不是线程池了,而是普通的多线程处理)

具体一点,一段提供如上功能的代码如下:(这个简陋的实现仅为演示上述思想,真正的实现需要对线程同步处理得更加规范,对错误意外情况做处理,完善其他必要的功能。P.s.这段代码是在word中编写,不能保证可以编译通过)

 

class ThreadPool;

void ThreadProc(PVOID param)

{

         ThreadPool * pThreadPool = reinterpret_cast< ThreadPool*>(param);

         ThreadTaskPool* pTaskPool = pThreadPool->GetTaskPool();

         while (true) {

         Task* pTask = pTaskPool->PopTask();

         If (!pTask)

                   Sleep(100);

         else

                   pTask->Do();

}

}

 

class Task {

public:

virtual void Do() = 0;

};

 

class ThreadTaskPool {

public:

ThreadTaskPool() { ::InitializeCriticalSection(&m_cs); }

Task* PopTask();

void PushTask(Task* pTask);

private:

CriticalSection m_cs;

std::queue<Task*> m_TaskQueue;

};

 

Task* ThreadTaskPool::PopTask()

{

         Lock<CriticalSection> _lock_;  // lock is a scope wrapper

         if (m_TaskQueue.empty())

                   return NULL;

         Task* pTask = m_TaskQueue.front();

         m_TaskQueue.pop_front();

         return pTask;

}

void ThreadTaskPool::PushTask(Task* pTask)

{

         Lock<CriticalSection> _lock_;

         m_TaskQueue.push_back(pTask);

         // we may do something else here(raise some event to notify the thread to wake up).

}

 

class ThreadPool {

public:

void Init();

ThreadTaskPool* GetTaskPool() const;

private:

HANDLE m_hThreads[MAX_THREADS];

ThreadTaskPool* m_pTasks;

};

 

void ThreadPool::Init()

{

         m_pTasks = new ThreadTaskPool;

         for(int nIdx = 0; nIdx < MAX_THREADS; ++nIdx)

         {

                  m_hThreads[nIdx] = CreateThread(NULL, 0, ThreadProc, this, 0, NULL);

}

}

 

上述代码实现了一个简单的线程池,Task接口用于定义任务,一切需要交给线程池处理的任务都派生自该接口。而线程池中的所有线程只在其线程处理函数中做一件事:检查任务管理器中是否能取出新的任务来,如果能就干活,不能就调用Sleep交出时间片。(这里有一个可以优化的地方,我们可以在任务管理器中定义一个事件对象,如果线程取不出任务,则等待该事件对象,如果有新任务被PushTask,则该事件对象被激活,并唤醒一个等待中的线程)

 

上面的两个例子:编辑器中的撤销/重做,以及线程池处理任务队列,都是实际编程当中对Command模式的典型应用。他们的共同点在于:都有一个统一的动作接口,而接口背后的工作可能多种多样。在实际编程当中遇到具有这种特点的问题,便可以采用这种思想进行抽象,有助于我们编写具有良好扩展性的程序。

Go to Top