3
gcc (GCC) 4.7.2

你好,

我正在开发一个包含我将开发的 2 个模块(共享库)的大型项目。

这些模块是我在 C 中创建的共享库,它们必须相互同步和交换消息。

在此处输入图像描述

管理器模块将控制这两个模块 (.so) 并将其加载到内存中。如果一个人失败了。经理可以尝试重新加载它。

我想知道,因为这是我第一次做这样的事情。有没有可以遵循的设计模式?

所有这些都将用 C 语言编写,并使用 APR(Apache Portable Runtime)进行内存池管理,如果需要,还可以使用一些线程池。

  1. 启动将加载两个模块的管理器。
  2. 然后,Manager 会在它们两个上调用一些函数来启动和停止它们,并可能进行清理。
  3. 一旦两个模块都已加载并启动。他们应该能够在彼此之间交换一些消息。

这些模块都将在运行 Redhat 的同一台机器上运行。

非常感谢您的任何建议。

4

7 回答 7

11

管理器模块将控制这两个模块 (.so) 并将其加载到内存中。如果一个人失败了。经理可以尝试重新加载它。

如果它在单个 C 进程中,这通常是一个坏主意——如果其中一个模块发生故障,您不太可能安全地卸载它,更不用说再次加载它了。如果您需要能够从模块故障中恢复,则必须使用独立进程。代码仍然可以在一个 .so 文件中 -fork()每个模块只需加载一次管理器;例如,这是 chrome 插件 API 使用的模型。

此外,处理组件故障本身可能非常非常棘手。仅仅因为 A 重新启动并不意味着 B 已准备好与新重新启动的 A 交谈。您可能想尝试从erlang中收集一些想法,它通过鼓励将应用程序分解为具有消息传递的子组件来出色地处理组件故障主管模块的层次结构以重新启动故障组件。如果您只有两个模块,这可能有点矫枉过正,但至少需要考虑一下。

至于如何沟通,有很多范式。如果这些模块在同一个进程中,你可以只传递一个 vtable。也就是说,例如:

// moduleA.h

struct vtable_A {
  void (*do_something)();
};

void set_vtable_B(struct vtable_B *);
struct vtable_A *get_vtable_A();
void start_A();

// moduleB.h
struct vtable_B {
  void (*do_something)();
};

void set_vtable_A(struct vtable_A *);
struct vtable_B *get_vtable_B();
void start_B();

您的经理将加载两者,将 vtable 从 A 传递到 B,反之亦然,然后调用启动例程。订购时要小心——要么 A 必须在 B 准备好之前启动,反之亦然,他们需要对此表示满意。

如果它们在独立的进程中,则消息传递通常是要走的路。那时它本质上是一个网络协议 - 您的子流程会将序列化消息发送到管理器,然后管理器将它们路由到其他子流程。对话可能看起来像这样:

MGR->A      START
MGR->B      START
A->MGR      REGISTER_ENDPOINT 'ProcessA'
A->MGR      WATCH_ENDPOINT 'ProcessB'
MGR->A      OK_REGISTER 'ProcessA'
MGR->A      OK_WATCH 'ProcessB'
B->MGR      REGISTER_ENDPOINT 'ProcessB'
B->MGR      WATCH_ENDPOINT 'ProcessA'
MGR->B      OK_REGISTER 'ProcessB'
MGR->A      NEW_ENDPOINT 'ProcessB'
A->MGR      APPLICATION_DATA TO:'ProcessB', PAYLOAD:"Hello, world!"
MGR->B      OK_WATCH 'ProcessA'
MGR->B      NEW_ENDPOINT 'ProcessA'
MGR->B      APPLICATION_DATA FROM:'ProcessA', PAYLOAD:"Hello, world!"

请记住,除了上面的示例之外,还有许多其他方法可以构建这种协议,并在消息传递协议之上构建 RPC。您可能有兴趣查看诸如DBUS(您可能可以直接使用!)或DCOM之类的东西,它们以前做过这种事情。在此类协议之上的其他优化包括使用管理器在 A 和 B 之间建立某种直接通道,并且仅在 A 或 B 需要重新启动时才再次参与。

也就是说,在弄清楚经理需要做什么之前,不要太深入了解经理的工作方式。设计plugin<->manager高层接口,plugin<->plugin协议;然后才设计插件<->管理器界面的细节。很容易偏离轨道并最终得到像CORBASOAP这样过于复杂的东西。

于 2012-10-04T06:54:53.217 回答
5

以下是基于您的请求的项目的简单示例:

您的源代码的体系结构可能是这样的:

src
   |__handler1.c //containing the main function
   |__handler2.c //containing other functions
   |__lib1.c //containing lib1 source
   |__lib2_file1.c  //containing lib2 source
   |__lib2_file2.c  //containing lib2 source
   |__Makefile  // file which contains commands to build the project
   |__inc
         |__lib1.h
         |__lib2.h
         |__handler2.h

handler1.c

#include <stdio.h>
#include "lib1.h"
#include "lib2.h"
#include "handler2.h"

int main()
{
    char *s1, *s2;
    print_hello_from_handler2();
    s1 = get_message_from_lib1_method1();
    get_message_from_lib1_method2(&s2);

    printf("s1 = %s\n",s1);
    printf("s2 = %s\n",s2);
    printf("extern string_from_lib1 = %s\n",string_from_lib1);
    printf("extern string_from_lib2 = %s\n",string_from_lib2);
}

handler2.c

#include <stdio.h>

void print_hello_from_handler2()
{
    printf("hello world from handler2\n");
}

lib1.c

#include "lib2.h"
char *string_from_lib1="message from lib1 variable";

char *get_message_from_lib1_method1()
{
    return get_message_from_lib2_method1();
}

void get_message_from_lib1_method2(char **s)
{
    get_message_from_lib2_method2(s);
}

lib2_file1.c

char *string_from_lib2="message from lib2 variable";

char *str="message from lib2 method1";

char *get_message_from_lib2_method1()
{
    return str;
}

lib2_file2.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void get_message_from_lib2_method2(char **s)
{
    *s = malloc(30);
    strcpy(*s,"message from lib2 method2");
}

lib1.h

extern char *string_from_lib1;

char *get_message_from_lib1_method1();
void get_message_from_lib1_method2(char **s);

lib2.h

extern char *string_from_lib2;

char *get_message_from_lib2_method1();
void get_message_from_lib2_method2(char **s);

handler2.h

void print_hello_from_handler2();

生成文件

SHLIB_EXT=so
LINK=$(CC)
SHLIB1_FILE=libmodule1.$(SHLIB_EXT).1
SHLIB2_FILE=libmodule2.$(SHLIB_EXT).1
SHLIB1_FLAGS=-shared -Wl,-soname,$(SHLIB1_FILE)
SHLIB2_FLAGS=-shared -Wl,-soname,$(SHLIB2_FILE)
FPIC=-fPIC

all: libmodule2.$(SHLIB_EXT) libmodule1.$(SHLIB_EXT) handler


%.o: %.c
    $(CC) -Iinc -c -o $@ $^

handler: handler1.o handler2.o
    $(CC) -o $@ $^ -L. -lmodule2 -lmodule1

lib2_file1.o: lib2_file1.c
    $(CC) $(FPIC) -Iinc -c -o $@ $<

lib2_file2.o: lib2_file2.c
    $(CC) $(FPIC) -Iinc -c -o $@ $<

libmodule2.$(SHLIB_EXT): lib2_file1.o lib2_file2.o
    $(LINK) $(SHLIB2_FLAGS) -o $(SHLIB2_FILE) $^
    ln -sf $(SHLIB2_FILE) $@

libmodule1.o: lib1.c
    $(CC) $(FPIC) -Iinc -c -o $@ $<

libmodule1.$(SHLIB_EXT): libmodule1.o
    $(LINK) $(SHLIB1_FLAGS) -o $(SHLIB1_FILE) $< -L. -lmodule2
    ln -sf $(SHLIB1_FILE) $@


clean:
    rm -f *.o *.so* handler
    rm -f /usr/lib/$(SHLIB1_FILE)
    rm -f /usr/lib/$(SHLIB2_FILE)
    rm -f /usr/lib/libmodule1.$(SHLIB_EXT)
    rm -f /usr/lib/libmodule2.$(SHLIB_EXT)

install:
    cp $(SHLIB1_FILE) /usr/lib/
    cp $(SHLIB2_FILE) /usr/lib/
    cp handler /usr/bin/
    ln -sf /usr/lib/$(SHLIB1_FILE) /usr/lib/libmodule1.$(SHLIB_EXT)
    ln -sf /usr/lib/$(SHLIB2_FILE) /usr/lib/libmodule2.$(SHLIB_EXT)

编译项目的命令

linux$ cd src
linux$ make

然后安装二进制文件和库

linux$ sudo make install

清理已安装的库和二进制文件并清理构建的二进制库和对象:

linux$ sudo make clean

要运行应用程序:

linux$ handler
hello world from handler2
s1 = message from lib2 method1
s2 = message from lib2 method2
extern string_from_lib1 = message from lib1 variable
extern string_from_lib2 = message from lib2 variable
linux$
于 2012-10-07T16:54:47.927 回答
2

我对“模式谈话”有点过敏,但这就是我的处理方式:

  • 确定线程模型。

    • 您的模块会使用它们控制的内存或管理器来交换信息吗?
    • 模块是否应该等待它们之间共享或由管理器拥有的条件变量?
  • 确定您需要的经理的通用性。

    • 它是否应该能够轮询模块的目录或读取配置或两者兼而有之?
    • 如果管理器管理消息,模块之间的信令需要什么?

当您知道这一点时,其余的应该主要是将存在于模块中的业务逻辑。

于 2012-10-01T09:29:30.247 回答
1

据我了解,您需要将第 1 点和第 2 点解耦。

  • 为此,您应该有一个名为 BootstrapManager 的单独类,它将负责加载模块并在它们失败时重新加载。
  • 接下来你需要一个名为 Module 的抽象类,它有 3 个方法,
    start() - 启动一个模块,stop() - 停止一个模块,cleanUp() - 清理活动,communicate() - 与另一个模块通信。
  • 现在 Module1 和 Module 2 都将扩展这个类并相应地实现自己的业务逻辑。
于 2012-10-05T17:48:16.753 回答
1

如果您已经决定使用 APR,您可能应该使用它提供的动态库加载您可以在此处找到教程 。

于 2012-10-10T10:33:24.347 回答
1

这里的架构相对简单,因此您不需要复杂的设计模式。

主要问题是数据完整性。如果系统部分崩溃,如何确保两者具有相同的数据副本?

由于您正在使用消息传递,因此您已经解决了一半的问题。你只需要做两件事:

(1) 存储最近消息列表并创建回滚/更新机制来恢复给定检查点备份的模块和自检查点以来的消息列表

(2) 确保消息是原子的;即,您永远不希望接受部分消息或事务,因为如果发送者在发送消息的过程中崩溃,则接收者可能会因接受不完整的信息而被破坏。

要解决问题 2,请在事务末尾添加校验和或哈希。除非接收到哈希并匹配数据,否则接收方不会最终确定其对消息集的接受。

于 2012-10-10T16:50:13.690 回答
1

一个关键问题:你为什么要以这种方式实现它?您正在“紧密”耦合本质上“松散”耦合的组件(因为共享库存在各种与崩溃相关的问题:它们会使管理器崩溃)。为什么不拥有一个(可以)在必要时启动和重新启动 2 个或更多子进程的管理器程序。

让子进程使用某种协议与 Manager 或彼此进行通信。我推荐ZeroMQ既因为它很棒,又因为它完全隐藏了进程间通信,所以它可以是套接字(不同机器之间),或者线程之间的进程间,或者单个机器上的命名管道,这非常快。这意味着在实现客户端之后,您可以决定如何部署它们:作为加载到管理器中的共享库、作为运行在同一机器上的单独进程或作为运行在不同机器上的分布式进程,您几乎不需要改变任何东西。这意味着非常可扩展。

但是,如果您致力于共享库方法,那么我绝对会推荐一种“设计模式”,尽管实现起来可能有点棘手。但这将是值得的。

你的经理,在模块之间传递消息之前,应该检查它们的时间戳,如果有任何改变,重新编译并重新加载它们。这意味着您的代码更改是“热的”:您不必停止管理器、重新编译并重新启动管理器即可查看更改。所以你可以用 C 编程更像用 js 开发!它将为您节省数小时。

不久前,我使用 C++ 和 APR 做了类似的事情(不是图书馆间通信)。代码有点“hacky”,但无论如何都在这里;-)

请注意,它取决于Makefile每个子模块在其自己的目录中都有一个,并且由于依赖关系,我不检查时间戳,我只是在每个请求上重新编译。这可能不适合您,因此您可能需要重新考虑该部分。

让它工作最困难的部分是获得对目录的正确权限,但仔细想想,那是因为我将它作为 fcgi 进程运行,所以当它实际运行时,它是作为网络服务器运行的。您很可能不会遇到这些问题。

#ifndef _CMJ_RUN_HPP
#define _CMJ_RUN_HPP

#include <fcgio.h>
#include <stdlib.h>

#include <iostream>
#include <string>
#include <sstream>
#include <vector>

#include <apr.h>
#include <apr_dso.h>
#include <apr_pools.h>
#include <apr_thread_proc.h>

#include <boost/filesystem.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/case_conv.hpp>

#include <cgicc/Cgicc.h>
#include <cgicc/HTTPHTMLHeader.h>
#include <cgicc/HTMLClasses.h>
#include <stdexcept>

#include <cstdarg>

class Line {
protected:
    std::stringstream line_;
    bool isError_;
public:
    Line(const char* line, bool isError) : line_(line), isError_(isError) {}
    Line(const Line& rhs) : line_(rhs.line()), isError_(rhs.error()) {}
    bool error() const { return isError_; }
    const char* line() const { return line_.str().c_str(); }
    const Line& operator = (const Line& rhs) {
        line_.str() = rhs.line();
        isError_ = rhs.error();
        return rhs;
    }
};

class Run {
protected:
    int exitCode_;
    std::vector<Line> out_;
    bool errors_;
protected:
    void run(const char* dir, const char* cmd, std::vector<const char*> &args, apr_pool_t* parentPool) ;
public:
    Run(const char* dir, const char* cmd, std::vector<const char*> &args, apr_pool_t* parentPool);
    Run(const char* dir, const char* cmd, apr_pool_t* parentPool);
    int exitCode() { return exitCode_; }
    bool errors() { return errors_; }
    bool errors(std::ostream& out);
    int size() { return out_.size(); }
    Line& line(int i) { return out_[i]; }
};

class dso_error: public std::runtime_error {
public:
    dso_error(const char* c) : std::runtime_error(c) {};
    dso_error(std::string err) : std::runtime_error(err) {};
    static dso_error instance(const char* format, ...) {
        char errbuf[8192];
        va_list va;
        va_start(va, format);
        vsnprintf(errbuf, 8192, format, va);
        va_end(va);
        return dso_error(errbuf);
    }
};

/**
 * Provides a building and loading framework for Dynamic libraries, with the full power
 * of make behind it.
 * Usage:
 * <code>
 * DsoLib so("/var/www/frontier/echo","/var/www/frontier/echo/libecho.so",pool);
 * if (!so.errors(outStream)) {
 *  void (*pFn)(void) = sym("initialize");
 *  (*pFn)();
 * }
 * </code>
 */
class DsoLib : public Run {
protected:
    apr_pool_t* pool_;
    apr_dso_handle_t* dso_;
    std::string dirname_;
    std::string libname_;
public:
    /** dir is the directory where make should be executed, libname is full path to the library
     * from current working directory.
     */
    DsoLib(const char* dir, const char* libname, apr_pool_t* parentPool) throw(dso_error);
    ~DsoLib();
    void* sym(const char* symbol) throw (dso_error);
    void* sym(std::string symbol) throw (dso_error) { return sym(symbol.c_str()); }
};

#endif

并运行.cpp

#include "Run.hpp"

#include <string>
#include <sstream>
#include <boost/filesystem.hpp>
#include <cassert>

#define DBGENDL " (" << __FILE__ << ":" << __LINE__ << ")" << endl


using namespace std;

Run::Run(const char* dir, const char* cmd, apr_pool_t* pool) : errors_(false) {
    vector<const char *> args;
    run(dir, cmd, args, pool);
}

Run::Run(const char* dir, const char* cmd, vector<const char*> &args, apr_pool_t* pool) : errors_(false) {
    run(dir, cmd, args, pool);
}

void
Run::run(const char* dir, const char* cmd, vector<const char*> &args, apr_pool_t* parentPool) {
    cout << "Run::run(dir=" << ", cmd=" << cmd << ", args...)" << endl;
    apr_status_t status;
    char aprError[1024];
    struct aprPool_s {
        apr_pool_t* pool_;
        aprPool_s(apr_pool_t* parent) {
            apr_pool_create(&pool_, parent);
        }
        ~aprPool_s() {
            apr_pool_destroy(pool_);
        }
        operator apr_pool_t*  () { return pool_; }
    } pool (parentPool);

    apr_procattr_t* attr;
    if (APR_SUCCESS != (status = apr_procattr_create(&attr, pool))) {
        cerr << "apr_procattr_create error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    if (APR_SUCCESS != (status = apr_procattr_dir_set(attr, dir))) {
        cerr << "apr_procattr_dir_set error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    if (APR_SUCCESS != (status = apr_procattr_cmdtype_set(attr, APR_PROGRAM_ENV))) {
        cerr << "apr_procattr_cmdtype_set error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    if (APR_SUCCESS != (status = apr_procattr_io_set(attr, APR_NO_PIPE, APR_FULL_NONBLOCK, APR_FULL_NONBLOCK))) {
        cerr << "apr_procattr_io_set error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    if (APR_SUCCESS != (status = apr_procattr_user_set(attr, "craig", "lateral"))) {
        cerr << "apr_procattr_user_set error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    if (APR_SUCCESS != (status = apr_procattr_group_set(attr, "craig"))) {
        cerr << "apr_procattr_group_set error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    apr_proc_t proc;

    const char **argv = (const char**) new char*[ 2 + args.size() ];
    argv[0] = cmd;
    size_t i=0;
    size_t argc=args.size();
    for (i=0; i<argc; i++) {
        argv[i+1] = args[i];
        cerr << "arg " << i << " = " << args[i];
    }
    argv[i+1] = NULL;
    argc++;
    cerr << "About to execute " << cmd << " in dir " << dir << endl;
    cerr << "ARGS:" << endl;
    for (i=0; i<argc; i++) {
        cerr << "[" << i << "]: " << argv[i] << endl;
    }

    if (APR_SUCCESS != (status = apr_proc_create(&proc, cmd, argv, NULL, attr, pool))) {
        cerr << "apr_proc_create error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }

    apr_exit_why_e exitWhy;
    cerr << "--- " << cmd << " ---" << endl;
    while (APR_CHILD_NOTDONE == (status = apr_proc_wait(&proc, &exitCode_, &exitWhy, APR_NOWAIT))) {
        char line[1024];
        status = apr_file_gets(line, sizeof(line), proc.out);
        if (APR_SUCCESS==status) {
            out_.push_back(Line(line, false));
            cerr << line << endl;
        }

        status = apr_file_gets(line, sizeof(line), proc.err);
        if (APR_SUCCESS==status) {
            out_.push_back(Line(line, true));
            errors_ = true;
            cerr << "E:" << line ;
        }
    }
    cerr << " -----" << endl;

    delete[] argv;

    if ( (APR_CHILD_DONE != status) && (APR_PROC_EXIT != status) ) {
        cerr << "apr_proc_wait error: " << apr_strerror(status, aprError, sizeof(aprError)) << endl;
    }
    cerr << cmd << " exited " << ((APR_PROC_EXIT==exitWhy) ? "PROC_EXIT" :
            ((APR_PROC_SIGNAL==exitWhy) ? "PROC_SIGNAL" :
            ((APR_PROC_SIGNAL_CORE==exitWhy) ? "PROC_SIGNAL_CORE" : "Unknown"))) << endl;
}

bool
Run::errors(std::ostream& os) {
    cerr << "Run::errors(ostream) : errors()=" << errors() << endl;
    if (errors()) {
        cerr << "Writing errors to ostream" << endl;
        os << "Content-type: text/html\r\n\r\n";
        os << "<html><head><title>Errors</title>"
            << "<link rel=\"stylesheet\" type=\"text/css\" href=\"css/frontier.css\"></link>"
            << "</head>"
            << "<body>";
        for (int i=0; i<size(); i++) {
            Line& errline = line(i);
            os << "<div class=\"" << ( (errline.error() ? "error" : "out" ) ) << "\">"
                    << errline.line()
                    << "</div>";
        }
        os
            << "</body>"
            << "</html>";
    }
    return errors();
}

DsoLib::DsoLib(const char* dir, const char* libname, apr_pool_t* parentPool) throw (dso_error) :
    Run(dir, "/usr/bin/make", parentPool), pool_(NULL), dso_(NULL), dirname_(dir), libname_(libname)
{
    if (errors()) {
        cerr << "Run encountered errors, quitting DsoLib::DsoLib()" << DBGENDL;
        //throw dso_error::instance("Build failed for dir %s, library %s", dir, libname);
        return;
    } else {
        cerr << "No errors encountered with Run in DsoLib::DsoLib" << DBGENDL;
    }

    apr_status_t status;
    if (APR_SUCCESS != apr_pool_create(&pool_, parentPool)) {
        cerr << "Failed to allocate pool" << DBGENDL;
        throw dso_error("Failed to allocate apr_pool");
    }

    cerr << "Created pool ok" << DBGENDL;   //(" << __FILE__ << ":" << __LINE__ << ")" << endl;

    if (APR_SUCCESS != (status = apr_dso_load(&dso_, libname, pool_))) {
        cerr << "apr_dso_load(" << libname << ") failed" << DBGENDL;
        char aprError[1024];
        throw dso_error::instance("dso_load failed, path=%s, error=%s",
                libname, apr_strerror(status, aprError, sizeof(aprError)));
    }
    cerr << "Loaded dso ok" << DBGENDL;
#if 0
    void (*initialize)(apr_pool_t*) = reinterpret_cast< void(*)(apr_pool_t*) > (sym("initialize"));
    if (initialize) {
        cerr << "found initialize sym: about to call initialize" << DBGENDL;
        initialize(pool_);
        cerr << "initialize(pool) returned ok" << DBGENDL;
    } else {
        cerr << "initialize sym not found" << DBGENDL;
    }
#endif
    cerr << "Exiting DsoLib::DsoLib(" << dir << ", " << libname << ") with success." << endl;
}

DsoLib::~DsoLib() {
    cerr << "Entering DsoLib::~DsoLib(dir=" << dirname_ <<", " << "lib=" << libname_ << ")" << endl;
    if (NULL!=dso_) {
        void (*terminate)(void) = reinterpret_cast<void(*)()>(sym("terminate"));
        if (terminate) terminate();
        apr_status_t status = apr_dso_unload(dso_);
        if (APR_SUCCESS != status) {
            char err[8192];
            cerr << "ERR apr_dso_unload failed: " << apr_dso_error(dso_, err, sizeof(err)) << endl;
        } else {
            cerr << "Unloaded " << libname_ << endl;
        }
    } else {
        cerr << "ERR dso_ handle is NULL" << endl;
    }
    if (NULL!=pool_) apr_pool_destroy(pool_);
}

void *
DsoLib::sym(const char* symbol) throw (dso_error) {
    cerr << "sym('" << symbol << "')" << DBGENDL;
    cerr << "dso_ == NULL ? " << ((NULL==dso_)?"true":"false") << DBGENDL;
    cerr << "dso_ = " << dso_ << DBGENDL;
    assert(NULL!=symbol);
    assert(NULL!=dso_);
    apr_status_t status;
    void* p = NULL;
    if (APR_SUCCESS != (status = apr_dso_sym((apr_dso_handle_sym_t*)&p, dso_, symbol))) {
        cerr << "apr_dso_sym() DID NOT RETURN APR_SUCCESS" << DBGENDL;
        char aprError[1024];
        stringstream err;
        err << "dso_sym failed, symbol=" << symbol << ": " << apr_strerror(status, aprError, sizeof(aprError));
        cerr << err.str() << DBGENDL;
    } else {
        cerr << "sym succeeded for " << symbol << " in " << libname_ << DBGENDL;
    }
    return p;
}
于 2012-10-11T01:21:02.123 回答