Làm thế nào để đối phó với xung đột ký hiệu giữa các thư viện được liên kết tĩnh?


82

Một trong những quy tắc quan trọng nhất và thực tiễn tốt nhất khi viết thư viện, là đặt tất cả các ký hiệu của thư viện vào một không gian tên cụ thể của thư viện. C ++ làm cho điều này dễ dàng, do namespacetừ khóa. Trong C, cách tiếp cận thông thường là đặt tiền tố định danh bằng một số tiền tố cụ thể của thư viện.

Các quy tắc của tiêu chuẩn C đặt ra một số ràng buộc đối với những điều đó (để biên dịch an toàn): Trình biên dịch AC có thể chỉ xem xét 8 ký tự đầu tiên của một số nhận dạng, do đó foobar2k_eggsfoobar2k_spamcó thể được hiểu là các số nhận dạng giống nhau một cách hợp lệ - tuy nhiên mọi trình biên dịch hiện đại đều cho phép các số nhận dạng dài tùy ý , vì vậy trong thời đại của chúng ta (thế kỷ 21) chúng ta không nên bận tâm về điều này.

Nhưng nếu bạn đang đối mặt với một số thư viện mà bạn không thể thay đổi tên biểu tượng / mã định danh? Có thể bạn chỉ có một tệp nhị phân tĩnh và tiêu đề hoặc không muốn, hoặc không được phép tự điều chỉnh và biên dịch lại.


Câu trả lời:


141

Ít nhất trong trường hợp thư viện tĩnh, bạn có thể làm việc xung quanh nó khá thuận tiện.

Hãy xem xét các tiêu đề thư viện foobar . Vì lợi ích của hướng dẫn này, tôi cũng sẽ cung cấp cho bạn các tệp nguồn

ví dụ / ex01 / foo.h

int spam(void);
double eggs(void);

example / ex01 / foo.c (cái này có thể không rõ ràng / không khả dụng)

int the_spams;
double the_eggs;

int spam()
{
    return the_spams++;
}

double eggs()
{
    return the_eggs--;
}

ví dụ / ex01 / bar.h

int spam(int new_spams);
double eggs(double new_eggs);

example / ex01 / bar.c (cái này có thể không rõ ràng / không khả dụng)

int the_spams;
double the_eggs;

int spam(int new_spams)
{
    int old_spams = the_spams;
    the_spams = new_spams;
    return old_spams;
}

double eggs(double new_eggs)
{
    double old_eggs = the_eggs;
    the_eggs = new_eggs;
    return old_eggs;
}

Chúng tôi muốn sử dụng chúng trong foobar chương trình

example / ex01 / foobar.c

#include <stdio.h>

#include "foo.h"
#include "bar.h"

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", spam(), eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
            spam(new_bar_spam), new_bar_spam, 
            eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Một vấn đề trở nên rõ ràng ngay lập tức: C không biết quá tải. Vì vậy, chúng ta có hai lần hai hàm có tên giống hệt nhau nhưng khác chữ ký. Vì vậy, chúng ta cần một số cách để phân biệt chúng. Dù sao, hãy xem trình biên dịch phải nói gì về điều này:

example/ex01/ $ make
cc    -c -o foobar.o foobar.c
In file included from foobar.c:4:
bar.h:1: error: conflicting types for ‘spam’
foo.h:1: note: previous declaration of ‘spam’ was here
bar.h:2: error: conflicting types for ‘eggs’
foo.h:2: note: previous declaration of ‘eggs’ was here
foobar.c: In function ‘main’:
foobar.c:11: error: too few arguments to function ‘spam’
foobar.c:11: error: too few arguments to function ‘eggs’
make: *** [foobar.o] Error 1

Được rồi, điều này không có gì ngạc nhiên, nó chỉ cho chúng tôi biết, những gì chúng tôi đã biết, hoặc ít nhất là nghi ngờ.

Vì vậy, bằng cách nào đó chúng ta có thể giải quyết xung đột nhận dạng đó mà không cần sửa đổi mã nguồn hoặc tiêu đề của thư viện ban đầu không? Trong thực tế, chúng tôi có thể.

Đầu tiên, hãy giải quyết các vấn đề về thời gian biên dịch. Đối với điều này, chúng tôi bao quanh tiêu đề bao gồm một loạt các #definechỉ thị tiền xử lý có tiền tố tất cả các ký hiệu được xuất bởi thư viện. Sau đó, chúng tôi thực hiện điều này với một số tiêu đề wrapper ấm cúng đẹp mắt, nhưng chỉ vì mục đích chứng minh những gì đang diễn ra đã làm nó nguyên văn trong tệp nguồn foobar.c :

ví dụ / ex02 / foobar.c

#include <stdio.h>

#define spam foo_spam
#define eggs foo_eggs
#  include "foo.h"
#undef spam
#undef eggs

#define spam bar_spam
#define eggs bar_eggs
#  include "bar.h"
#undef spam
#undef eggs

int main()
{
    const int    new_bar_spam = 3;
    const double new_bar_eggs = 5.0f;

    printf("foo: spam = %d, eggs = %f\n", foo_spam(), foo_eggs() );
    printf("bar: old spam = %d, new spam = %d ; old eggs = %f, new eggs = %f\n", 
           bar_spam(new_bar_spam), new_bar_spam, 
           bar_eggs(new_bar_eggs), new_bar_eggs );

    return 0;
}

Bây giờ nếu chúng ta biên dịch cái này ...

example/ex02/ $ make
cc    -c -o foobar.o foobar.c
cc   foobar.o foo.o bar.o   -o foobar
bar.o: In function `spam':
bar.c:(.text+0x0): multiple definition of `spam'
foo.o:foo.c:(.text+0x0): first defined here
bar.o: In function `eggs':
bar.c:(.text+0x1e): multiple definition of `eggs'
foo.o:foo.c:(.text+0x19): first defined here
foobar.o: In function `main':
foobar.c:(.text+0x1e): undefined reference to `foo_eggs'
foobar.c:(.text+0x28): undefined reference to `foo_spam'
foobar.c:(.text+0x4d): undefined reference to `bar_eggs'
foobar.c:(.text+0x5c): undefined reference to `bar_spam'
collect2: ld returned 1 exit status
make: *** [foobar] Error 1

... có vẻ như mọi thứ trở nên tồi tệ hơn. Nhưng hãy nhìn kỹ lại: Trên thực tế, giai đoạn biên dịch diễn ra tốt đẹp. Nó chỉ là trình liên kết hiện đang phàn nàn rằng có các biểu tượng va chạm và nó cho chúng ta biết vị trí (tệp nguồn và dòng) nơi điều này xảy ra. Và như chúng ta có thể thấy những biểu tượng đó không được định sẵn.

Hãy cùng xem các bảng ký hiệu với tiện ích nm :

example/ex02/ $ nm foo.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

example/ex02/ $ nm bar.o
0000000000000019 T eggs
0000000000000000 T spam
0000000000000008 C the_eggs
0000000000000004 C the_spams

Vì vậy, bây giờ chúng ta được thử thách với bài tập đặt tiền tố các ký hiệu đó trong một số nhị phân không rõ ràng. Vâng, tôi biết trong quá trình ví dụ này, chúng tôi có các nguồn và có thể thay đổi điều này ở đó. Nhưng hiện tại, hãy giả sử bạn chỉ có các tệp .o hoặc .a (thực ra chỉ là một loạt .o ).

phản đối sự giải cứu

Có một công cụ đặc biệt thú vị đối với chúng tôi: mục tiêu

objcopy hoạt động trên các tệp tạm thời, vì vậy chúng tôi có thể sử dụng nó như thể nó đang hoạt động tại chỗ. Có một tùy chọn / thao tác được gọi là - ký hiệu tiền tố và bạn có 3 lần đoán nó hoạt động.

Vì vậy, hãy ném cái này vào các thư viện cứng đầu của chúng ta:

example/ex03/ $ objcopy --prefix-symbols=foo_ foo.o
example/ex03/ $ objcopy --prefix-symbols=bar_ bar.o

nm cho chúng ta thấy rằng điều này dường như hoạt động:

example/ex03/ $ nm foo.o
0000000000000019 T foo_eggs
0000000000000000 T foo_spam
0000000000000008 C foo_the_eggs
0000000000000004 C foo_the_spams

example/ex03/ $ nm bar.o
000000000000001e T bar_eggs
0000000000000000 T bar_spam
0000000000000008 C bar_the_eggs
0000000000000004 C bar_the_spams

Hãy thử liên kết toàn bộ điều này:

example/ex03/ $ make
cc   foobar.o foo.o bar.o   -o foobar

Và thực sự, nó đã hoạt động:

example/ex03/ $ ./foobar 
foo: spam = 0, eggs = 0.000000
bar: old spam = 0, new spam = 3 ; old eggs = 0.000000, new eggs = 5.000000

Bây giờ tôi để nó như một bài tập cho người đọc để thực hiện một công cụ / tập lệnh tự động trích xuất các ký hiệu của thư viện bằng cách sử dụng nm , viết tệp tiêu đề trình bao bọc của cấu trúc

/* wrapper header wrapper_foo.h for foo.h */
#define spam foo_spam
#define eggs foo_eggs
/* ... */
#include <foo.h>
#undef spam
#undef eggs
/* ... */

và áp dụng tiền tố biểu tượng cho các tệp đối tượng của thư viện tĩnh bằng cách sử dụng objcopy .

Điều gì về thư viện chia sẻ?

Về nguyên tắc, điều tương tự cũng có thể được thực hiện với các thư viện dùng chung. Tuy nhiên, cái tên đã nói lên rằng các thư viện được chia sẻ được chia sẻ giữa nhiều chương trình, vì vậy việc xáo trộn một thư viện được chia sẻ theo cách này không phải là một ý kiến ​​hay.

Bạn sẽ không phải lo lắng khi viết một tấm bạt lò xo. Tệ hơn nữa là bạn không thể liên kết với thư viện được chia sẻ ở cấp tệp đối tượng mà buộc phải thực hiện tải động. Nhưng điều này xứng đáng với bài viết của riêng nó.

Hãy theo dõi và viết mã vui vẻ.


4
Thật ấn tượng! Không ngờ rằng nó lại dễ dàng như vậy objcopy.
Kos

12
Bạn vừa ... trả lời câu hỏi của chính mình trong vòng 1 phút sau khi hỏi nó?
Alex B

18
@Alex B: Đây là một bài viết hướng dẫn và tôi đã đi theo một đường dẫn được gợi ý cho tôi tại meta.stackoverflow.com cách người ta có thể đặt các hướng dẫn về các câu hỏi (thú vị?) Và giải pháp của chúng. Câu hỏi về các thư viện xung đột xuất hiện và tôi nghĩ "hm, tôi biết cách đối phó với loại giải pháp này", đã viết một bài báo và đăng nó ở đây dưới dạng Q&A. meta.stackexchange.com/questions/97240/…
datenwolf

4
@datenwolf bất kỳ ý tưởng nào về cách giải quyết vấn đề này cho các thư viện iOS. Như tôi đã phát hiện ra, objcopy không hỗ trợ thư viện iOS: /
Ege Akpinar

6
Blah blah blah objcopy --prefix-symbols ... +1!
Ben Jackson

7

Các quy tắc của tiêu chuẩn C đặt ra một số ràng buộc đối với những điều đó (để biên dịch an toàn): Trình biên dịch AC có thể chỉ xem xét 8 ký tự đầu tiên của một số nhận dạng, vì vậy foobar2k_eggs và foobar2k_spam có thể được hiểu là cùng một số nhận dạng hợp lệ - tuy nhiên mọi trình biên dịch hiện đại đều cho phép tùy ý số nhận dạng dài, vì vậy trong thời đại của chúng ta (thế kỷ 21) chúng ta không cần phải bận tâm về điều này.

Đây không chỉ là một phần mở rộng của các trình biên dịch hiện đại; tiêu chuẩn C hiện tại cũng yêu cầu trình biên dịch hỗ trợ các tên bên ngoài dài hợp lý. Tôi quên độ dài chính xác nhưng bây giờ nó giống như 31 ký tự nếu tôi nhớ đúng.

Nhưng nếu bạn đang đối mặt với một số thư viện mà bạn không thể thay đổi tên biểu tượng / mã định danh? Có thể bạn chỉ có một tệp nhị phân tĩnh và tiêu đề hoặc không muốn, hoặc không được phép tự điều chỉnh và biên dịch lại.

Sau đó, bạn bị mắc kẹt. Khiếu nại với tác giả của thư viện. Tôi đã từng gặp một lỗi như vậy trong đó người dùng ứng dụng của tôi không thể xây dựng nó trên Debian do libSDLliên kết của Debian libsoundfile, điều này (ít nhất là vào thời điểm đó) đã làm ô nhiễm không gian tên toàn cầu một cách khủng khiếp với các biến như dsp(Tôi không nghĩ vậy!). Tôi đã khiếu nại với Debian, và họ đã sửa các gói của họ và gửi bản sửa lỗi ngược dòng, nơi tôi cho rằng nó đã được áp dụng, vì tôi chưa bao giờ nghe nói về sự cố lần nữa.

Tôi thực sự nghĩ rằng đây là cách tiếp cận tốt nhất, vì nó giải quyết được vấn đề cho mọi người . Bất kỳ hack cục bộ nào mà bạn thực hiện sẽ để lại vấn đề trong thư viện cho người dùng không may tiếp theo gặp phải và chiến đấu lại.

Nếu bạn thực sự cần một bản sửa lỗi nhanh và bạn có nguồn, bạn có thể thêm một loạt -Dfoo=crappylib_foo -Dbar=crappylib_barv.v. vào makefile để sửa nó. Nếu không, hãy sử dụng objcopygiải pháp bạn đã tìm thấy.


Tất nhiên là bạn đúng, tuy nhiên đôi khi bạn cần một bản hack bẩn thỉu, như cách tôi đã trình bày ở trên. Ví dụ: nếu bạn bị mắc kẹt với một số thư viện cũ mà nhà cung cấp đã ngừng kinh doanh hoặc tương tự. Tôi đặc biệt viết điều này cho các thư viện tĩnh .
datenwolf

3

Nếu bạn đang sử dụng GCC, công tắc trình liên kết - cho phép nhiều định nghĩa là một công cụ gỡ lỗi tiện dụng. Điều này khuyến khích trình liên kết sử dụng định nghĩa đầu tiên (và không than vãn về nó). Thêm về nó ở đây .

Điều này đã giúp tôi trong quá trình phát triển khi tôi có sẵn nguồn cho một thư viện do nhà cung cấp cung cấp và cần theo dõi vào một chức năng thư viện vì lý do này hay lý do khác. Công tắc cho phép bạn biên dịch và liên kết trong một bản sao cục bộ của tệp nguồn và vẫn liên kết đến thư viện nhà cung cấp tĩnh chưa sửa đổi. Đừng quên kéo công tắc ra khỏi các biểu tượng chế tạo sau khi hành trình khám phá hoàn tất. Mã phát hành vận chuyển với các xung đột không gian tên có chủ ý dễ gặp phải các cạm bẫy bao gồm các va chạm không gian tên không chủ ý .

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.