Thư viện chia sẻ động C ++ trên Linux


167

Đây là phần tiếp theo để biên dịch Thư viện chia sẻ động với g ++ .

Tôi đang cố gắng tạo một thư viện lớp chia sẻ trong C ++ trên Linux. Tôi có thể lấy thư viện để biên dịch và tôi có thể gọi một số hàm (không phải lớp) bằng cách sử dụng các hướng dẫn mà tôi tìm thấy ở đâyđây . Vấn đề của tôi bắt đầu khi tôi cố gắng sử dụng các lớp được xác định trong thư viện. Hướng dẫn thứ hai mà tôi đã liên kết để chỉ ra cách tải các biểu tượng để tạo các đối tượng của các lớp được xác định trong thư viện, nhưng không sử dụng các đối tượng đó để hoàn thành bất kỳ công việc nào.

Có ai biết một hướng dẫn đầy đủ hơn để tạo các thư viện lớp C ++ được chia sẻ cũng cho thấy cách sử dụng các lớp đó trong một tệp thực thi riêng biệt không? Một hướng dẫn rất đơn giản cho thấy việc tạo đối tượng, sử dụng (getters và setters đơn giản sẽ ổn), và xóa sẽ là tuyệt vời. Một liên kết hoặc một tham chiếu đến một số mã nguồn mở minh họa việc sử dụng thư viện lớp dùng chung sẽ tốt như nhau.


Mặc dù các câu trả lời từ codelogicnimrodm đều có tác dụng, tôi chỉ muốn thêm rằng tôi đã chọn một bản sao của Lập trình Linux bắt đầu kể từ khi hỏi câu hỏi này, và chương đầu tiên của nó có mã C và giải thích tốt cho việc tạo và sử dụng cả thư viện tĩnh và chia sẻ . Những ví dụ này có sẵn thông qua Tìm kiếm Sách của Google trong phiên bản cũ hơn của cuốn sách đó .


Tôi không chắc tôi hiểu ý của bạn khi "sử dụng" nó, khi một con trỏ tới đối tượng được trả về, bạn có thể sử dụng nó giống như bạn sử dụng bất kỳ con trỏ nào khác cho một đối tượng.
codelogic

Bài viết tôi liên kết để chỉ ra cách tạo một con trỏ hàm đến một hàm nhà máy đối tượng bằng cách sử dụng dlsym. Nó không hiển thị cú pháp để tạo và sử dụng các đối tượng từ thư viện.
Bill Lizard

1
Bạn sẽ cần tệp tiêu đề mô tả lớp. Tại sao bạn nghĩ rằng bạn phải sử dụng "dlsym" thay vì chỉ để HĐH tìm và liên kết thư viện khi tải? Hãy cho tôi biết nếu bạn cần một ví dụ đơn giản.
nimrodm

3
@nimrodm: Điều gì thay thế cho việc sử dụng "dlsym"? Tôi (được cho là) ​​đang viết 3 chương trình C ++, tất cả sẽ sử dụng các lớp được định nghĩa trong thư viện dùng chung. Tôi cũng có 1 tập lệnh Perl sẽ sử dụng nó, nhưng đó là một vấn đề hoàn toàn khác cho tuần tới.
Lập hóa đơn cho thằn lằn

Câu trả lời:


154

mygroup.h

#ifndef __MYCLASS_H__
#define __MYCLASS_H__

class MyClass
{
public:
  MyClass();

  /* use virtual otherwise linker will try to perform static linkage */
  virtual void DoSomething();

private:
  int x;
};

#endif

mygroup.cc

#include "myclass.h"
#include <iostream>

using namespace std;

extern "C" MyClass* create_object()
{
  return new MyClass;
}

extern "C" void destroy_object( MyClass* object )
{
  delete object;
}

MyClass::MyClass()
{
  x = 20;
}

void MyClass::DoSomething()
{
  cout<<x<<endl;
}

class_user.cc

#include <dlfcn.h>
#include <iostream>
#include "myclass.h"

using namespace std;

int main(int argc, char **argv)
{
  /* on Linux, use "./myclass.so" */
  void* handle = dlopen("myclass.so", RTLD_LAZY);

  MyClass* (*create)();
  void (*destroy)(MyClass*);

  create = (MyClass* (*)())dlsym(handle, "create_object");
  destroy = (void (*)(MyClass*))dlsym(handle, "destroy_object");

  MyClass* myClass = (MyClass*)create();
  myClass->DoSomething();
  destroy( myClass );
}

Trên Mac OS X, biên dịch với:

g++ -dynamiclib -flat_namespace myclass.cc -o myclass.so
g++ class_user.cc -o class_user

Trên Linux, biên dịch với:

g++ -fPIC -shared myclass.cc -o myclass.so
g++ class_user.cc -ldl -o class_user

Nếu đây là một hệ thống plugin, bạn sẽ sử dụng MyClass làm lớp cơ sở và xác định tất cả các hàm cần thiết là ảo. Tác giả plugin sau đó sẽ lấy từ MyClass, ghi đè lên các ảo và thực hiện create_objectdestroy_object. Ứng dụng chính của bạn sẽ không cần phải thay đổi theo bất kỳ cách nào.


6
Tôi đang trong quá trình thử điều này, nhưng chỉ có một câu hỏi. Có thực sự cần thiết phải sử dụng void *, hoặc có thể hàm created_object trả về MyClass * không? Tôi không yêu cầu bạn thay đổi điều này cho tôi, tôi chỉ muốn biết liệu có lý do để sử dụng cái này hơn cái kia không.
Lập hóa đơn cho thằn lằn

1
Cảm ơn, tôi đã thử cái này và nó hoạt động như trên Linux từ dòng lệnh (một khi tôi đã thực hiện thay đổi mà bạn đề xuất trong các nhận xét mã). Tôi đánh giá cao thời gian của bạn.
Bill Lizard

1
Có bất kỳ lý do nào bạn sẽ khai báo những điều này với "C" bên ngoài không? Vì điều này được biên dịch bằng trình biên dịch g ++. Tại sao bạn muốn sử dụng quy ước đặt tên c? C không thể gọi c ++. Một giao diện trình bao bọc được viết bằng c ++ là cách duy nhất để gọi nó từ c.
ant2009

6
@ ant2009 bạn cần extern "C"dlsymhàm này là hàm C. Và để tự động tải create_objectchức năng, nó sẽ sử dụng liên kết kiểu C. Nếu bạn không sử dụng extern "C", sẽ không có cách nào biết tên của create_objecthàm trong tệp .so, vì việc xáo trộn tên trong trình biên dịch C ++.
kokx

1
Phương pháp hay, nó rất giống với những gì ai đó sẽ làm trên trình biên dịch Microsoft. với một chút công việc #if #else, bạn có thể có được một hệ thống độc lập nền tảng tốt đẹp
Ha11owed

52

Sau đây cho thấy một ví dụ về thư viện lớp dùng chung được chia sẻ. [H, cpp] và mô-đun main.cpp sử dụng thư viện. Đó là một ví dụ rất đơn giản và makefile có thể được làm tốt hơn nhiều. Nhưng nó hoạt động và có thể giúp bạn:

shared.h định nghĩa lớp:

class myclass {
   int myx;

  public:

    myclass() { myx=0; }
    void setx(int newx);
    int  getx();
};

shared.cpp định nghĩa các hàm getx / setx:

#include "shared.h"

void myclass::setx(int newx) { myx = newx; }
int  myclass::getx() { return myx; }

main.cpp sử dụng lớp,

#include <iostream>
#include "shared.h"

using namespace std;

int main(int argc, char *argv[])
{
  myclass m;

  cout << m.getx() << endl;
  m.setx(10);
  cout << m.getx() << endl;
}

và tệp thực hiện tạo lib Shared.so và liên kết chính với thư viện dùng chung:

main: libshared.so main.o
    $(CXX) -o main  main.o -L. -lshared

libshared.so: shared.cpp
    $(CXX) -fPIC -c shared.cpp -o shared.o
    $(CXX) -shared  -Wl,-soname,libshared.so -o libshared.so shared.o

clean:
    $rm *.o *.so

Để thực sự chạy 'chính' và liên kết với lib Shared.so có thể bạn sẽ cần chỉ định đường dẫn tải (hoặc đặt nó trong / usr / local / lib hoặc tương tự).

Sau đây chỉ định thư mục hiện tại là đường dẫn tìm kiếm cho các thư viện và chạy hàm chính (cú pháp bash):

export LD_LIBRARY_PATH=.
./main

Để thấy rằng chương trình được liên kết với lib Shared.so bạn có thể thử ldd:

LD_LIBRARY_PATH=. ldd main

In trên máy của tôi:

  ~/prj/test/shared$ LD_LIBRARY_PATH=. ldd main
    linux-gate.so.1 =>  (0xb7f88000)
    libshared.so => ./libshared.so (0xb7f85000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7e74000)
    libm.so.6 => /lib/libm.so.6 (0xb7e4e000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb7e41000)
    libc.so.6 => /lib/libc.so.6 (0xb7cfa000)
    /lib/ld-linux.so.2 (0xb7f89000)

1
Điều này dường như (với con mắt rất chưa được đào tạo của tôi) là liên kết tĩnh với lib Shared.so với khả năng thực thi của bạn, thay vì sử dụng liên kết động trong thời gian chạy. Tôi có đúng không?
Xuất hóa đơn cho thằn lằn

10
Không. Đây là liên kết động Unix (Linux) tiêu chuẩn. Một thư viện động có phần mở rộng ".so" (Đối tượng chia sẻ) và được liên kết với tệp thực thi (chính trong trường hợp này) tại thời điểm tải - mỗi lần tải chính. Liên kết tĩnh xảy ra tại thời điểm liên kết và sử dụng các thư viện có phần mở rộng ".a" (lưu trữ).
nimrodm

9
Điều này được liên kết động tại thời điểm xây dựng . Nói cách khác, bạn cần có kiến ​​thức trước về thư viện mà bạn đang liên kết (ví dụ: liên kết với 'dl' cho dlopen). Điều này khác với việc tải động một thư viện, dựa trên tên tệp do người dùng chỉ định, trong đó không cần có kiến ​​thức trước.
codelogic

10
Điều tôi đã cố gắng giải thích (thật tệ) là trong trường hợp này, bạn cần biết tên của thư viện khi xây dựng (bạn cần chuyển -l Shared cho gcc). Thông thường, người ta sử dụng dlopen () khi thông tin đó không có sẵn, tức là tên của thư viện được phát hiện trong thời gian chạy (ví dụ: liệt kê plugin).
codelogic

3
Sử dụng -L. -lshared -Wl,-rpath=$$(ORIGIN)khi liên kết và thả nó LD_LIBRARY_PATH=..
Maxim Egorushkin

9

Về cơ bản, bạn nên bao gồm tệp tiêu đề của lớp trong mã nơi bạn muốn sử dụng lớp trong thư viện dùng chung. Sau đó, khi bạn liên kết, hãy sử dụng cờ '-l' để liên kết mã của bạn với thư viện dùng chung. Tất nhiên, điều này đòi hỏi .so phải là nơi HĐH có thể tìm thấy nó. Xem 3.5. Cài đặt và sử dụng thư viện dùng chung

Sử dụng dlsym là khi bạn không biết lúc biên dịch thư viện nào bạn muốn sử dụng. Điều đó không giống như trường hợp ở đây. Có lẽ sự nhầm lẫn là Windows gọi các thư viện được tải động cho dù bạn thực hiện liên kết tại thời gian biên dịch hoặc thời gian chạy (với các phương thức tương tự)? Nếu vậy, thì bạn có thể nghĩ dlsym tương đương với LoadL Library.

Nếu bạn thực sự cần phải tải động các thư viện (nghĩa là chúng là các trình cắm thêm), thì Câu hỏi thường gặp này sẽ giúp ích.


1
Lý do tôi cần một thư viện chia sẻ động là tôi cũng sẽ gọi nó từ mã Perl. Đó có thể là một quan niệm sai lầm hoàn toàn về phía tôi mà tôi cũng cần phải gọi nó một cách linh hoạt từ các chương trình C ++ khác mà tôi đang phát triển.
Bill Lizard

Tôi chưa bao giờ thử tích hợp perl và C ++, nhưng tôi nghĩ bạn cần sử dụng XS: johnkeiser.com/perl-xs-c++.html
Matt Lewis

5

Trên các câu trả lời trước, tôi muốn nâng cao nhận thức về thực tế rằng bạn nên sử dụng thành ngữ RAII (Tài nguyên thu nhận tài nguyên) để an toàn về việc phá hủy xử lý.

Dưới đây là một ví dụ hoạt động hoàn chỉnh:

Khai báo giao diện Interface.hpp::

class Base {
public:
    virtual ~Base() {}
    virtual void foo() const = 0;
};

using Base_creator_t = Base *(*)();

Nội dung thư viện dùng chung:

#include "Interface.hpp"

class Derived: public Base {
public:
    void foo() const override {}
};

extern "C" {
Base * create() {
    return new Derived;
}
}

Trình xử lý thư viện chia sẻ động Derived_factory.hpp::

#include "Interface.hpp"
#include <dlfcn.h>

class Derived_factory {
public:
    Derived_factory() {
        handler = dlopen("libderived.so", RTLD_NOW);
        if (! handler) {
            throw std::runtime_error(dlerror());
        }
        Reset_dlerror();
        creator = reinterpret_cast<Base_creator_t>(dlsym(handler, "create"));
        Check_dlerror();
    }

    std::unique_ptr<Base> create() const {
        return std::unique_ptr<Base>(creator());
    }

    ~Derived_factory() {
        if (handler) {
            dlclose(handler);
        }
    }

private:
    void * handler = nullptr;
    Base_creator_t creator = nullptr;

    static void Reset_dlerror() {
        dlerror();
    }

    static void Check_dlerror() {
        const char * dlsym_error = dlerror();
        if (dlsym_error) {
            throw std::runtime_error(dlsym_error);
        }
    }
};

Mã khách hàng:

#include "Derived_factory.hpp"

{
    Derived_factory factory;
    std::unique_ptr<Base> base = factory.create();
    base->foo();
}

Ghi chú:

  • Tôi đặt mọi thứ trong các tập tin tiêu đề cho thống nhất. Trong cuộc sống thực, bạn tất nhiên nên phân chia mã của mình giữa .hpp.cppcác tệp.
  • Để đơn giản hóa, tôi bỏ qua trường hợp bạn muốn xử lý new/ deletequá tải.

Hai bài viết rõ ràng để biết thêm chi tiết:


Đây là một ví dụ tuyệt vời. RAII chắc chắn là con đường để đi.
David Steinhauer
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.