Tải và chạy thư viện động C++ trên Linux
C++
30
Linux
81
White

cpplover viết ngày 27/05/2015

Thư viện động giúp giải quyết một số vấn đề trong lập trình mà nhiều khi việc link tĩnh (static) không giải quyết được. Ví dụ: chạy function do người dùng cung cấp trên nền tảng của mình.

Để tải và chạy function của thư viện liên kết động, Linux hỗ trợ hàm dlopen, dlerror, dlsym.

Các hàm này chỉ hỗ trợ C function mà không hỗ trợ C++ function. Lý do là: C++ hỗ trợ overloaded functions (hàm cùng tên khác biến số) nên tên hàm sau khi được biên dịch đã bị mangling đi. Nếu ta chỉ định tên mangling, ta vẫn có thể dùng dlsym để tải hàm đó lên. Thế nhưng quy tắc mangling của các compiler là khác nhau, thậm chí trong cùng một compiler thì quy tắc thay đổi khi version thay đổi, do vậy ta không nên chỉ định tên mangled. Ngoài ra ta chỉ tải được hàm chứ không tải được class hay đối tượng. Do vậy ta cần một cách khác để tải động C++ function và đối tượng.

Ví dụ về name mangling
alt text

Bài viết này trình bày phương pháp tải và chạy hàm trong thư viện động của C++ trên Linux.

Tải một hàm

Để tải một hàm, ta phải không cho phép C++ compiler mangling tên hàm đó đi. Trong C++ ta có thể dùng khai báo externn "C" để compiler không mangling tên hàm.

hello.cpp

#include <iostream>

extern "C" void hello() {
    std::cout << "Hello world\n";
}

Biên dịch dùng command sau (mình dùng clang)

$ clang++ -dynamiclib hello.cpp -o hello.dylib
$ ls 
hello.dylib

Tiếp theo ta viết chương trình tải hàm hello lên.

loader.cpp

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

int main() {
    void* handle = dlopen("./hello.dylib", RTLD_LAZY);
    typedef void (*hello_t)();
    dlerror();
    hello_t hello = (hello_t) dlsym(handle, "hello");
    hello();
    dlclose(handle);
}
$ clang++ loader.cpp -o loader
$ ./loader
Hello world

Tải một class

Để tải một class object và gọi hàm trong object đấy ta phải biết trong đấy có hàm gì. Ta có thể định nghĩa 1 virtual class làm interface. Ở đây minh định nghĩa 1 class tên là Task có hàm Calc. Các hàm người dùng sẽ thừa kế Task và tự định nghĩa Calc cho riêng mình.

Task.h

#ifndef TASK_H
#define TASK_H

class Task {
public:
  Task() { }
  virtual ~Task() {}

  virtual void Calc() = 0;
};

typedef Task* create_t();
typedef void destroy_t(Task*);

#define REGISTER_TASK(ClassName)                \
  extern "C" ClassName* create() {              \
    return new ClassName;                       \
  }                                             \
                                                \
  extern "C" void destroy(ClassName* p) {       \
    delete p;                                   \
  }                                             \

#endif /* TASK_H */

Task có hàm Calc cần được override class con. Ngoài ra class hỗ trợ một macro là REGISTER_TASK để cung cấp 2 hàm createdestroy. 2 hàm này dùng để tại và thu hồi bộ nhớ cấp phát cho đối tượng Task. Ta cũng định nghĩa 2 kiểm create_tdestroy_t là con trỏ hàm là kiểu mà ta sẽ tải tự động lên.

Tiếp theo mỗi đối tượng người dùng định nghĩa có thể thừa kế Task và viết mã tính toán vào hàm Calc của mình như sau:

PrintTask

#include "Task.h"
#include <iostream>
#include <cmath>

class PrintTask: public Task {
 public:
  virtual void Calc() {
    std::cout << "Sin(Pi/2) = " << std::sin(M_PI/2) << std::endl;
  }
};

REGISTER_TASK(AreaTask)

Hàm đơn giản tính Sin(Pi/2) và in kết quả ra stdout.

Tiếp theo ta cần một Runner chuyên tải thư viện chỉ định và chạy. Ta có đoạn code như sau:

TaskRunner.cpp

#include <iostream>
#include <stdio.h>
#include <dlfcn.h>
#include "Task.h"

int main(int argc, char **argv) {
  if (argc < 2) {
    std::cerr << "Usage: TaskRunner dlib\n";
    return -1;
  }
  const char *filename = argv[1];

  // Load library
  void *handle = dlopen(filename, RTLD_LAZY);
  if (!handle) {
    std::cerr << "Cannot load library: " << dlerror() << "\n";
    return 1;
  }

  // reset error
  dlerror();

  create_t *create_task = (create_t*)dlsym(handle, "create");
  const char* dlsym_error = dlerror();
  if (dlsym_error) {
    std::cerr << "Cannot load symbol creat: " << dlsym_error << "\n";
    return 1;
  }

  destroy_t *destroy_task = (destroy_t*)dlsym(handle, "destroy");
  dlsym_error = dlerror();
  if (dlsym_error) {
    std::cerr << "Cannot load symbol destroy: " << dlsym_error << "\n";
    return 1;
  }

  Task* task = create_task();
  task->Calc();
  destroy_task(task);

  dlclose(handle);
  return 0;
}

Ta tải một đối tượng dùng hàm create_task. Do hàm này được định nghĩa là một C interface nên tên hàm không bị mangling. Hàm làm nhiệm vụ trả về đối tượng được đăng ký bởi REGISTER. Ta ép kiểu đối tượng trả về thành Task, do đối tượng trong thư viện động thừa kế từ Task và có chung interface với Task. Sau đó ta gọi hàm tính Calc thực hiện tính toán.

Thử chạy

$ clang++ -dynamiclib PrintTask.cpp -o PrintTask.dylib
$ clang++ TaskRunner.cpp -o TaskRunner
$ ./TaskRunner ./PrintTask.dylib
Sin(Pi/2) = 1

Hàm của ta thực hiện tải thành công hàm Calc định nghĩa trong đối tượng PrintTask và chạy nó!

Kết luận

Tải một object bằng cách đưa ra một C interface function. Function này trả về một đối tượng. Định nghĩa một đối tượng interface để thống nhất Interface chạy.

Các bài sau sẽ giới thiệu nhiều hơn về các ứng dụng của kỹ thuật này.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

cpplover

12 bài viết.
44 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
16 2
Giới thiệu Cách duy nhất để học C++ trong 21 ngày là dành hơn 14611 ngày để nghiên cứu và thay thế chính bạn ở ngày số 21 (Ảnh) C++ là một ngôn ...
cpplover viết hơn 3 năm trước
16 2
White
16 4
C++ 11 có chức năng lambda. Vì là ngôn ngữ "static type" nên C++ lambda function nhìn khó hơn lambda trong các ngôn ngữ dynamic type khác nhưng pyt...
cpplover viết gần 3 năm trước
16 4
White
15 0
Lang thang hacker news tìm được một phần nhỏ (Link) của một bác ở Anh tổng hợp về stack mà google đã tạo và sử dụng. Chi tiết về các stack được tổ...
cpplover viết 3 năm trước
15 0
Bài viết liên quan
Male avatar
0 0
Constructor của lớp cha luôn được gọi trước constructor của lớp con. class Foo { public: Foo() { cout << "Base class initializing" << endl; ...
baoquocphan viết 1 năm trước
0 0
White
10 1
Chức năng pattern matching trong C++ thấy nói sẽ kill (Link). Cách viết C++ do vậy sẽ thay đổi tương đối. Tự nhiên tôi thấy phải nhìn lại visitor ...
cpplover viết 3 năm trước
10 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
12 bài viết.
44 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!