C++封装C库

5

最近我找到了一个C库,想在我的C++项目中使用它。这个代码配置了全局变量并将其输出写入由静态指针指向的内存中。当我执行我的项目时,我希望运行两个C程序实例:一个是配置A,另一个是配置B。我无法承担两次运行程序的成本,所以我认为有两个选择:

  • 制作C++包装器:问题在于包装器类应该包含C库具有的所有全局/静态变量。由于C库中的函数使用这些变量,因此我必须为这些函数创建非常大的参数列表。
  • 复制粘贴 C库:在这里,我将不得不调整C库中每个函数和变量的名称。

哪一个是最快的解决方案?是否有其他运行相同C源的两个实例的可能性?

谢谢,

Max

5个回答

3

C++包装器
将“整个库”(略有修改)粘贴到一个类中,可以更轻松地解决问题。

// C
static char resultBuffer[42];
void ToResult(int x) { ... }
char const * GetResult() { return resultBuffer; }

变成

// C++
class CMyImportantCLib
{
  private:
    char resultBuffer[42];
    void ToResult(int x) { ... } // likely, no code changes at all
    char const * GetResult() { return resultBuffer; }
} ;

大部分都是声明性更改(例如“杀死”静态和外部声明)。然而,您需要在方法中查找静态变量,并将其转换为成员变量。

独立的命名空间
这是一种不太优雅的解决方案,但可能对您有用:

// impMyLib.h
namespace A 
{
  #include "c-lib.h"
}
namespace B
{
  #include "c-lib.h"
}

// impMyLib.cpp
namespace A 
{
  #include "c-lib.c"
}
namespace B
{
  #include "c-lib.c"
}

如果你很幸运,优化器和链接器会成功折叠相同的代码。然而,在A::B::中的类型是不相关的。


@paercebal, @peterchen:最终我通过粘贴整个库并查找静态/外部/全局变量来完成它。另一个困难是编写析构函数。 - Maximilien

2
IIUC,您的基本情况是这样的:
extern int a;
extern int b;

void f();
void g(); 

ab 修改 f()g() 的行为时,这句话是否正确?

如果您有这个需求并想在 C++ 中进行包装,可以这样做:

class the_library {
public:
  the_library(int a, int b) : a_(a), b_(b) {}

  void f() {a=a_; b=b_; ::f();}
  void g() {a=a_; b=b_; ::g();}
private:
  int a_;
  int b_;

};

根据实际情况,如果你替换了ab的话,这可能不是非常高效的。

当然,正如Raki在评论中所说,由于使用全局变量,这不是线程安全的。


值得一提的是,这种方法仅适用于单线程应用程序。 - Rakis
1
@Rakis:由于该库是使用全局变量进行配置的,我认为这是不言而喻的。 - sbi
这条评论是为了那些可能在未来看到这个帖子的C++新手而写的。我可以看出这个话题很容易被搜索引擎找到。“不言而喻”是相对于你的经验水平而言的。还记得大学物理教授说过这句话吗?是的。 - Rakis
@Rakis:你说得有道理。我会在回答中加一句话。谢谢。 - sbi

2
如果你无法运行两次,那么三次如何?你可以编写一个小型前端进程来启动两个单独的C程序实例。从使用角度来看,它仍然看起来像是一个单独的.exe文件,只需运行一次,但在幕后,你将拥有一个父进程和两个子进程。我不知道这种方法是否适合你的实际需求,但它几乎肯定比你其他两个选项更快。

那么这两个配置也需要相互通信吗? - Mark B
假设我有一个名为Bar的项目,并创建了两个进程foo1和foo2,它们各自拥有独立的配置。在Bar运行期间,它需要向foo1和foo2传输信息。 在运行的最后,Bar会收集foo1和foo2收集到的信息。 - Maximilien

1

也许有什么我错过了的地方,但是...

...全局变量在线程之间共享,而不是进程...

这意味着在您的情况下,您可以有相同C程序的两个进程正在工作,并且它们不会相互干扰,除非它们与进程共享内存相关。

...如果您需要在同一进程中运行C代码的两个实例...

那么你就卡住了。

TLS,也许?

要么你可以将它们启动到单独的线程中,并将全局变量声明为线程本地存储变量。例如,在Visual C++上,以下代码:

int myGlobalVariable = 42 ;                 // Global variable
__declspec(thread) int myTLSVariable = 42 ; // Thread local variable

每个线程都会有自己的变量版本。这样,在线程结束时,您可以将内容复制到其他地方。

重写代码...

您不需要为此添加C++层。您可以保留您的C代码,并在结构体中声明所有全局变量:

/* C global variable */
int iMyGlobalVariable = 42 ;
const char * strMyGlobalString = NULL ;
short iMyShortData = 7 ;

/* C struct */
typedef struct MyStruct
{
   int iMyGlobalVariable ;
   const char * strMyGlobalString ;
   short iMyShortData ;
}
MyStruct ;

然后您修改函数的原型,使其接受指向此结构体的指针作为第一个参数,然后不再修改全局变量,而是修改结构体成员:
/* old function */
int foo(char *p)
{
   /* fudge with the global variables */
   iMyShortData = 55 ;

   /* etc. */
   fooAgain("Hello World", 42) ;
}

变成了:

/* new function */
int foo(MyStruct * s, char *p)
{
   /* fudge with the struct variables */
   s->iMyShortData = 55 ;

   /* etc. */
   fooAgain(s, "Hello World", 42) ;
}

然后,在主函数中,你不再直接调用第一个函数,而是通过传递正确结构体的指针来调用它。不再使用:

int main(int argc, char * argv[])
{
   bar(42, 55) ;
}

您编写:

int main(int argc, char * argv[])
{
   MyStruct A = { /* initialize A's members if needed */ }  ;
   MyStruct B = { /* initialize B's members if needed */ }  ;

   bar(&A, 42, 55) ;
   bar(&B, 42, 55) ;

   return 0 ;
}

在上面的例子中,这两个被依次调用,但是你可以使用线程来启动它们。

如何保存全局状态?

如果你的代码是单线程的,你可以通过保存/重置全局状态来交错调用第一个实例和第二个实例。让我们使用上面相同的结构体:
/* C global variable */
int iMyGlobalVariable = 42 ;
short iMyShortData = 7 ;

void saveState(MyStruct * s)
{
   s->iMyGlobalVariable = iMyGlobalVariable ;
   s->iMyShortData = iMyShortData ;
}

void resetState(const MyStruct * s)
{
   iMyGlobalVariable = s->iMyGlobalVariable ;
   iMyShortData = s->iMyShortData ;
}

然后,需要时调用保存和重置函数:

int main(int argc, char * argv[])
{
   MyStruct A = { /* initialize A's members if needed */ }  ;
   MyStruct B = { /* initialize B's members if needed */ }  ;

   resetState(&A) ; /* now, we work on A */
   bar(42, 55) ;
   saveState(&A) ;  /* we save the progress on A */

   resetState(&B) ; /* now, we work on B */
   bar(42, 55) ;
   saveState(&B) ;  /* we save the progress on B */

   resetState(&A) ; /* now, we work on A */
   foo("Hello World", 3.14159) ;
   saveState(&A) ;  /* we save the progress on A */

   resetState(&B) ; /* now, we work on B */
   foo("Hello World", 3.14159) ;
   saveState(&B) ;  /* we save the progress on B */

   /* etc. */
   return 0 ;
}

这可以通过C++代码进行包装,以自动包装resetState/saveState函数。例如:

struct MyWrapper
{
    void foo(const char * p, double d)
    {
       resetState(&m_s) ;
       foo(p, d) ;
       saveState(&m_s) ;
    }

    void bar(int i, short i2)
    {
       resetState(&m_s) ;
       bar(i, i2) ;
       saveState(&m_s) ;
    }

    MyStruct m_s ;
} ;

你可以启用重写主函数为:

int main(int argc, char * argv[])
{
   MyWrapper A ;
   MyWrapper B ;

   A.bar(42, 55) ;
   B.bar(42, 55) ;

   A.foo("Hello World", 3.14159) ;
   B.foo("Hello World", 3.14159) ;

   // etc.

   return 0 ;
}

这看起来比C版本好多了。不过,MyWrapper并不是线程安全的...

结论

第一种解决方案(TLS)是快速且简单的解决方案,而第二种则是重构代码以正确编写它(全局变量被反对有很好的理由,显然,你遇到了其中之一),第三种是一种“hack”,使您可以交错两个调用。

在这三种解决方案中,只有第二种解决方案将使其易于包装此代码在健壮的、线程安全的C++类中(如果仍然需要)。


0

我喜欢这里的想法。但是我应该为我需要修改的每个变量创建一个指针。 这里有一个例子:

lib.h:

void f();
int g();

lib.c:

#include "lib.h"
extern int a;
extern int * output;

void f(){
    *output=(*output+6)*a;
}
int g(){
    return *output;
}

object.cc:

#include "lib.h"
#include <iostream>
using namespace std;

int a;
int * output;

class the_library {
public:
  the_library(int a, int * output) : a_(a), output_(output) {}

  void f() {a=a_; output=output_; ::f();}
  int g() {a=a_; output=output_; ::g();}
private:
  int a_;
  int * output_;

};

int main(){

    int out1=2;
    the_library icache(3,&out1);
    icache.f();
    cout<<"icache.f() -> icache is "<<icache.g()<<endl;
    icache.f();
    cout<<"icache.f() -> icache is "<<icache.g()<<endl;

    int out2;
    out2=8;
    the_library dcache(7,&out2);
    dcache.f();
    cout<<"dcache.f()\t-> icache is "<<icache.g()<<endl;
    cout<<"\t\t-> dcache is "<<dcache.g()<<endl;
    return 0;
}

我不确定“制作每个需要修改的变量的指针”是什么意思。与“int a; int b;”不同,你有“int a; int* output;”,但除此之外没有任何区别。我有什么遗漏吗? - sbi
我的意思是: 假设您的C代码中有2个全局变量: int a; int output; 您的C库的输出写入“output”。当您想要编写这样的C++包装器时,您需要创建一个指向内存的指针,而不仅仅是一个变量。 - Maximilien
你是否(错误地)认为指针不是变量? - sbi
顺便提一下,你应该在回复我的评论时以@sbi开头,这样SO就有机会通知我了。我是偶然发现上面那个的。 - sbi
@sbi:谢谢你的提示 ;-). 是的,如果我用“原始非指针数据类型”代替“变量”,会更清楚一些。 - Maximilien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接