Sharp as C






2.27/5 (8投票s)
2005年7月14日
10分钟阅读

34250
本文代表了一种通用的应用程序架构和设计方法。
引言
1:18 智慧越多,忧愁也越多
增加知识的,也增加忧愁。
KJV - 传道书
起初是…一个词。词是…一个算法!?或者我应该说 al-khwarizm? 维基百科怎么说算法这个词?
引用:“算法(这个词来源于波斯数学家 Al-Khwarizmi 的名字)是一组有限的、明确定义的指令,用于完成某项任务,给定一个初始状态,它将终止于一个可识别的相应结束状态”。
Al-Khwarizmi?引用:“Abu Abdullah Muhammad bin Musa al-Khwarizmi,是一位波斯科学家、数学家、天文学家/占星家和作家。他可能出生在 780 年,或大约 800 年;并可能死于 845 年,或大约 840 年。”
1200 年!
这篇文章是关于什么的?
以及如何阅读它。本文代表了我对软件开发及其架构的通用方法的个人观点。为了节省您宝贵的时间,我将隐藏我所有的想法和过程,无论它们是长还是短,只向您提供最终结论。有时这些结论可能看起来…奇怪,但这就是我的想法,我的个人意见。在这篇文章中,我只是在表达我自己的观点,无论您是否理解,无论您是否看到任何有用的想法,或者您认为所有这些都完全无用,都由您决定。
文章的计划很简单:在“应该如何”部分,我将描述总体思路。如果您觉得有趣,请继续阅读“这是可能的”部分,您将在其中找到实现的详细信息。其他一切只是“应该如何”部分提出的方法的优缺点。如果“应该如何”部分描述的想法对您来说毫无意义,您可以节省时间,不必继续阅读。
应该如何
“……因为简洁是智慧的灵魂,冗长则是其四肢和外在的装饰,我将言简意赅:你的高贵儿子疯了:”
波洛涅斯,《哈姆雷特,丹麦王子》,莎士比亚
能否画出一个应用程序的通用架构?有人可能会说这取决于业务。IMHO:它应该看起来像图 1 所示。
图 1
业务逻辑将用脚本编写,以便尽可能简单。业务实体是你业务所需的任何东西,例如集合(向量、映射、集合等)、日志系统、数据库供应商(MSSQL、Oracle、SyBase 等)、IPC(DCOM、RPC、套接字、管道)、线程系统(posix 可能是一个好例子)等等,所有这些实体都应该暴露某种简单的接口,基本上是 getters 和 setters。这些实体在逻辑上应该尽可能简单,总的来说,我认为它们应该只导出数据或简单的功能。业务逻辑将用某种脚本语言编写,并且为了绝对性,这意味着也要支持可移植性。如果某个实体要被更改(例如,您从 SQL 切换到 Oracle),在理想情况下,逻辑不应改变。我的意思是,任何业务逻辑和实体都应该分开。让我们看一个经典的例子
int main() { printf (“Hello world.\n”); return 0; }
在这个例子中,业务逻辑以 C 脚本的形式表示(总的来说这是脚本,因为我们不知道如何启动它)。只有一个业务实体,即 C 库(例如 libc、msvcrt),它暴露了简单的“导出” C 函数(在本例中为printf
)。参见图 2。
图 2
这种方法与传统的面向对象开发方法背道而驰。面向对象将对象及其功能捆绑在一起,而这种方法则不然。它甚至这样保持事物清洁并试图节省一些时间。我敢说,面向对象已经耗尽了它的资源,而且它是…死了,IMHO。
现在我(并且这些也是我的 IMHO)说,开发业务实体不像开发业务逻辑算法那么痛苦。构建业务算法是一个更奇特、更痛苦、更神经质的过程,并且比其他任何事情都需要更多的时间和资源。
因此,业务逻辑将用纯脚本编写,并且此脚本可以在运行时更改,而无需任何重新编译。我的基本目标是,业务逻辑不应成为“圣地”,一旦工作就永远不变,否则就很荒谬。它应该在需要时“可玩”,尤其是在开发/QA 阶段。此逻辑/脚本可以在运行时更改,而无需进行任何提交/签入,无需重新构建、重启,所有这些恼人的程序,只需简单地更改脚本就应立即影响正在运行的系统。让我猜猜,您会说不可能,或者如果可能——太复杂了。
这是可能的
而且并不复杂。本节将展示它是如何工作的。(示例代码是为 Microsoft Windows 平台编写的。)
这是一个应用程序树
├───c_dispatcher
├───Debug
├───frontend_app
├───include
├───my_script
├───my_script_c_proxy
└───my_script_d_proxy
在 fronend_app 文件夹内,是主(控制台)应用程序。里面只有一个文件:frontend_app.cpp。
// frontend_app.cpp // (c) George Shagov, 2005 #include <windows.h> #include "..\\include\my_structs.h" typedef int (__cdecl *MYFARPROC)(int nArg, char* pString, SMyStructure* pMyStruct); int main(int argc, char* argv[]) { HMODULE hMyScript = LoadLibrary("my_script_d_proxy.dll "); MYFARPROC pProcSource = (MYFARPROC)GetProcAddress(hMyScript, "c__my_entry_point"); SMyStructure myStruct; myStruct.m_nVal = 0; strcpy(myStruct.m_sString, ""); /* * calling for entry point. * directly */ char sMyString[32]; strcpy(sMyString, "My string here."); pProcSource(argc, sMyString, &myStruct); return 0; }
从这里可以看到,它获取脚本入口点的地址并执行它。脚本本身可以在 my_scipt 文件夹中找到,文件名是:my_script.c_。那里还有一些附加文件:my_script.gnrtd.c、my_script.gnrtd.h;这些将从 my_script.c_ 生成。
这是脚本
// my_script.c_ // (c) George Shagov, 2005 /************************************************************************ * * this file is automatically generated from my_script.c_ * do not modify it * ************************************************************************/ #include <stdio.h> #include <string.h> #include "..\\include\\my_structs.h" #include "my_script.gnrtd.h" int c__get_value_1_impl(char* pString) { return 1; } int c__get_value_2_impl(int nArg) { return 2; } int c__call_in_case_varables_are_equal_impl(SMyStructure* pMyStruct) { pMyStruct->m_nVal = 0; strcpy(pMyStruct->m_sString, "equal"); return 0; } int c__call_in_case_varables_are_not_equal_impl(SMyStructure* pMyStruct) { pMyStruct->m_nVal = 0; strcpy(pMyStruct->m_sString, "not equal"); return 0; } int c__re_entry_impl(int nArg, char* pString, SMyStructure* pMyStruct) { int nVar1 = c__get_value_1(pString); int nVar2 = c__get_value_2(nArg); if (nVar1 == nVar2) { c__call_in_case_varables_are_equal(pMyStruct); } else { c__call_in_case_varables_are_not_equal(pMyStruct); } return 11; } int c__my_entry_point_impl(int nArg, char* pString, SMyStructure* pMyStruct) { int nRet; printf("-----------\nbefore:\n"); printf("nArg: %d, string: %s\n", nArg, pString); printf("pMyStruct->m_nVal: %d, pMyStruct->m_sString: %s\n", pMyStruct->m_nVal, pMyStruct->m_sString); nRet = c__re_entry(nArg, pString, pMyStruct); printf("++++++after:\n"); printf("nArg: %d, string: %s\n", nArg, pString); printf("pMyStruct->m_nVal: %d, pMyStruct->m_sString: %s\n", pMyStruct->m_nVal, pMyStruct->m_sString); printf("ret: %d\n-------------\n", nRet); return nRet; }
c__my_entry_point_impl
是由 frontend_app
调用入口点。my_script.gnrtd.c 是原始脚本的简单副本。my_script.gnrtd.h 表示声明。
正如您所见,fronend_app
使用 my_script_d_proxy 库来调用 c__my_entry_point_impl
。my_script_d_proxy 文件夹下有两个文件:my_script_d_proxy.gnrtd.c 和 my_script_d_proxy.gnrtd.h,这两个文件都将从原始脚本(my_script.c_)生成。my_script_d_proxy.gnrtd.c 包含脚本中所有函数的占位符,如下所示
int c__re_entry_stub(int nESP, int nArg, char* pString, SMyStructure* pMyStruct) { void* pArgs = 0; int nSize = 0; _asm { push eax; /* saving eax */ mov eax, ebp; /* ebp points out at the parameters (as known) */ add eax, 8; /* now eax points out at the first argument, which is nESP*/ mov pArgs, eax; add pArgs, 4; /* since first argument is esp, but we need real argument here */ mov eax, nESP; sub eax, pArgs; /* eax now has a phisical size of the stack */ shr eax, 2; /* eax/4 - eax now has an amount of arguments put in the stack */ mov nSize, eax; /* saving that size */ pop eax; /* restoring eax */ } return g_pDispatcherEntry("c__re_entry", pArgs, nSize); } int c__re_entry(int nArg, char* pString, SMyStructure* pMyStruct) { int nESP; _asm { mov nESP, esp; } return c__re_entry_stub(nESP, nArg, pString, pMyStruct); }
汇编器指令会记住堆栈中第一个参数的指针和堆栈中的参数数量,然后调用 c_dispatcher 库,该库导出 g__c_dispatcher_entry_point
函数。
c_dispatcher.cpp 的代码
// c_dispatcher.cpp // (c) George Shagov, 2005 #include <stdio.h> #include <windows.h> #include "c_dispatcher.h" static HINSTANCE s_hCSource = NULL; static HINSTANCE s_hProxy = NULL; typedef int (__cdecl *MYFARPROC)(); MYFARPROC GetMyProcAddress(const char* pFunctionName) { char pFile[128]; char pFnName[128]; sprintf(pFile, "my_script.%s_impl.c_", pFunctionName); sprintf(pFnName, "%s_impl", pFunctionName); FILE* f = fopen(pFile, "r"); if (f) { fclose(f); return (MYFARPROC)GetProcAddress(s_hProxy, pFnName); } else return (MYFARPROC)GetProcAddress(s_hCSource, pFnName); } BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: s_hCSource = LoadLibrary("my_script.dll"); s_hProxy = LoadLibrary("my_script_c_proxy.dll"); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: FreeLibrary(s_hCSource); FreeLibrary(s_hProxy); break; } return TRUE; } // This is an example of an exported function. C_DISPATCHER_API int g__c_dispatcher_entry_point(const char* pFunctionName, const void* pArguments, int nArgumentsCount) { MYFARPROC pProc = GetMyProcAddress(pFunctionName); void* pStack = 0; if (nArgumentsCount) { _asm { mov ecx, nArgumentsCount; loop_start_01: push 0; loop loop_start_01; mov pStack, esp; } memcpy(pStack, pArguments, nArgumentsCount*4); int nRet = pProc(); _asm { mov ecx, nArgumentsCount; loop_start_02: pop eax; loop loop_start_02; } return nRet; } else return pProc(); }
从这里可以看到,如果调度程序找到文件 my_script.<function_name_impl>.c_,它会将调用委托给 my_script_c_proxy 库,否则则委托给 my_script.dll,其中包含编译后的脚本代码。这实际上是一种替换。在调用之前,它会模拟堆栈,知道原始堆栈的指针及其大小;调用之后——简单地回溯。很简单,对吧?
my_script_c_proxy 库包含四个文件。(这里我应该说,由于我们将在运行时更改代码,因此我们需要某种 C 解释器。我选择了 Cint。Cint 是一个免费的 C 解释器,功能强大,非常适合此演示,但存在一些问题,这意味着此演示实现中的一些缺点将与此特定解释器紧密相关。)G__clink.c、G__clink.h – 这些文件由 Cint 从 my_script_d_proxy.gnrtd.h(my_script_d_proxy 文件夹)生成,因为 Cint 在解释时不需要调用脚本函数,而是调用 my_script_d_proxy 库中的 stub,这样您就可以重新实现任何您需要的函数,而不是整个脚本。其余的函数将从 my_script.dll 调用。有点棘手。文件 my_script_c_proxy.gnrtd.c 包含如下所示的 stub
MY_SCRIPT_C_PROXY_API int c__my_entry_point_impl(int nArg, char* pString, SMyStructure* pMyStruct) { char tmp[128]; int nRet; s__setup_cint(); sprintf(tmp,"c__my_entry_point_impl((int)%d, (void*)0x%08lx, (SMyStructure*)0x%08lx);", nArg, (int)pString, pMyStruct); nRet = G__calc(tmp).obj.i; /* Call Cint parser */ return nRet; }
G__calc
是一个 Cint 函数,它调用脚本。嗯,实际上就是这样。
让我们看看它是如何工作的。
项目构建完成后,c_\Debug 文件夹的上下文如下所示
C_dispatcher.dll
frontend_app.exe
my_script.dll
my_script_c_proxy.dll
my_script_d_proxy.dll
启动应用程序,我们得到
-----------
before:
nArg: 1, string: My string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString:
++++++after:
nArg: 1, string: My string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString: not equal
ret: 11
-------------
这是由编译后的脚本生成的,现在位于 m_script.dll 库中。
现在在 Debug 文件夹中,我们创建一个空文件:my_script.c__get_value_1_impl.c_。此文件的存在将作为信号告知调度程序,c__get_value_1_impl
函数有一个替换项。我们还需要在 my_script.c_ 文件中创建,内容如下(两个文件的存在是由于 Cint 导致的那个缺点)。
// my_script.cpp : Defines the entry point for the DLL application. // #include <stdio.h> #include "..\\include\\my_structs.h" int c__get_value_1_impl(char* pString) { pString[1] = 'X'; printf("c__get_value_1 ==>> str: %s\n", pString); return 2; }
c_\Debug 文件夹的上下文如下所示
C_dispatcher.dll
frontend_app.exe
my_script.c_
my_script.c__re_entry_impl.c_
my_script.dll
my_script_c_proxy.dll
my_script_d_proxy.dll
重新启动应用程序,得到结果
-----------
before:
nArg: 1, string: My string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString:
c__get_value_1 ==>> str: MX string here.
++++++after:
nArg: 1, string: MX string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString: equal
ret: 11
-------------
现在让我们尝试重新实现两个函数。为此,我们创建第二个文件:my_script.c__re_entry_impl.c_,以向调度程序发出信号,并修改脚本
// my_script.cpp : Defines the entry point for the DLL application. // #include <stdio.h> #include "..\\include\\my_structs.h" int c__get_value_1_impl(char* pString) { pString[1] = 'X'; printf("c__get_value_1 ==>> str: %s\n", pString); return 2; } int c__re_entry_impl(int nArg, char* pString, SMyStructure* pMyStruct) { printf("\"I'll not be juggled with.\nTo hell, allegiance! Vows, to the blackest devil!\nConscience and grace, to the profoundest pit!\nI dare damnation. To this point I stand,\"\n"); printf("...for this is script\n"); int nVar1 = c__get_value_1(pString); int nVar2 = c__get_value_2(nArg); if (nVar1 == nVar2) { c__call_in_case_varables_are_equal(pMyStruct); } else { c__call_in_case_varables_are_not_equal(pMyStruct); } return 11; }
结果
-----------
before:
nArg: 1, string: My string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString:
"I'll not be juggled with.
To hell, allegiance! Vows, to the blackest devil!
Conscience and grace, to the profoundest pit!
I dare damnation. To this point I stand,"
...for this is script
c__get_value_1 ==>> str: MX string here.
++++++after:
nArg: 1, string: MX string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString: equal
ret: 11
-------------
现在来谈谈函数参数。我可能已经注意到字符串“My string here”已更改为“MX string here”。这是通过在脚本中重新实现的 c__get_value_1_impl
完成的。我们能够对结构做同样的事情。创建一个新文件:my_script.c__call_in_case_varables_are_equal_impl.c_ 并将以下函数添加到脚本中
int c__call_in_case_varables_are_equal_impl(SMyStructure* pMyStruct) { pMyStruct->m_nVal = 0; strcpy(pMyStruct->m_sString, "-- EQUAL --"); return 0; }
结果
-----------
before:
nArg: 1, string: My string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString:
"I'll not be juggled with.
To hell, allegiance! Vows, to the blackest devil!
Conscience and grace, to the profoundest pit!
I dare damnation. To this point I stand,"
...for this is script
c__get_value_1 ==>> str: MX string here.
++++++after:
nArg: 1, string: MX string here.
pMyStruct->m_nVal: 0, pMyStruct->m_sString: -- EQUAL --
ret: 11
-------------
此时 c_\Debug 文件夹的上下文如下所示
c_dispatcher.dll
frontend_app.exe
my_script.c_
my_script.c__call_in_case_varables_are_equal_impl.c_
my_script.c__get_value_1_impl.c_
my_script.c__re_entry_impl.c_
my_script.dll
my_script_c_proxy.dll
my_script_d_proxy.dll
它奏效了。
正如您所见
- 可以在运行时更改(或者更确切地说,替换)代码(脚本),无需重新编译。
- 这不是一件难事。
性能
是的,当然,使用脚本而不是本地代码确实意味着性能会显着下降,但有两点需要说明
- 在性能至关重要的系统中,例如实时系统,不允许进行任何替换。这意味着不应该有任何调度程序库,并且所有调用都应作为直接调用进行编译,并在编译期间链接。通过这种方法,不会有任何性能损失。然而,在开发和 QA 中,替换是高度要求的,但性能并不重要,这种方法将适用。
- 总的来说,性能不是关键点。在这种情况下,如果我们需要在生产环境中进行替换,并且不会显着损失性能,那也是可能的。为了做到这一点,我们应该
- 创建一个并编译一个单独的库,让它命名为 my_scipt_subst.dll。该库将包含需要替换的函数的重新实现。
- 创建并编译一个额外的代理库,让它命名为 my_script_s_prioxy.dll,它应该看起来像 my_script_c_prioxy.dll,请注意,所有调用都将委托给 my_scipt_subst.dll(参见步骤 a。),而不是 Cint。
- 修改调度程序,使其知道 my_script_s_prioxy.dll 的存在。
我没有这样做,以免代码过于冗长。如果基本思想是可理解的,其余的——只是技术。
优点和缺点
如果我有耐心和时间,我会在这里写一本书,或者两本书。但简而言之。
缺点
- 构建过程变得更加复杂,需要额外的解析。
- 应该提供一个解释器。
- 请参阅性能部分。
- 使用 C 作为脚本可能会导致一些问题,因为 C 默认情况下具有直接内存访问权限,并且没有自动回溯机制,这可能会导致内存泄漏。然而,这应该是 C 脚本,而不是 C,这意味着所有访问内存的函数都应该作为实体暴露。
优点
- 清晰度。面向对象代码的可读性远低于纯脚本。而这 IMHO 是主要目标。
- 能够在运行时更改业务逻辑。
- 控制。只要想想我们能够掌控所有入口点。
待办事项
很多。
- 应该有一个合适的 C 解释器。
- 解析过程。
- 参见“性能”部分中描述的第二条子句。
- 调度程序。是的,当然,它的实现方式不适用于真实系统。应该有一个函数映射表,该表将根据修改的时间戳在单独的线程中进行更新。
- 等等……