【问题标题】:Detect and use optional external C library at runtime in Objective-C在 Objective-C 运行时检测和使用可选的外部 C 库
【发布时间】:2015-11-22 13:03:35
【问题描述】:

我正在构建一个 SDK,iPhone 开发人员可以将其包含在他们的项目中。它作为已编译的“.a”交付,没有源代码。让我们将我的 SDK 称为“AAA”。

项目中的客户(我们称之为“BBB”)除了使用 AAA 外,还可能使用名为“CCC”的第 3 方库——它也是预编译的闭源库。我不卖 CCC,这是一家不同的公司。

我的 SDK,AAA,可以选择使用 CCC 来改进产品,使用这些 3rd 方功能。例如,假设 CCC 是一个用于加密某些东西的安全 SDK。 AAA 不需要 CCC,但如果客户也选择在他们的项目中包含 CCC,则会更加安全。

现在这里有一个特别棘手的部分 - CCC 库,是纯 C 代码,由 C 结构和 C 函数组成 - 与它无关。

问题是:

  • 如何编译我的 AAA SDK 以使用来自 CCC 的函数/结构,而不在我的项目中包含 CCC(法律不允许,并且不想跟上版本更新)。
  • 如何检测客户的项目中是否有 CCC,以便仅在可用时使用这些额外功能?

【问题讨论】:

  • 通常库定义一些标志通知每个人它的礼物。看看,也许CCC会?然后你可以使用#ifdef/#elif/#endif 预处理命令
  • this 可能会有所帮助。
  • @mas'an 我想到了这一点,他们确实定义了 #def - 但 #ifdef 是编译时功能。我正在向客户交付预编译的代码...
  • 您能否创建第三个库仅用于包装 CCC 库?

标签: ios objective-c c static-libraries late-binding


【解决方案1】:

你可以使用弱函数来做到这一点。 在您的静态库中,声明您要使用的所有 ccc 函数,如下所示:

int cccfunction(void) __attribute__((weak));

不要在你的库中包含 ccc。 由于函数被声明为弱函数,编译器不会抱怨它们的缺失,但是您可以在代码中引用它。 然后,当您将库分发给用户时,给他们一个 .c 文件,其中包含空的 ccc 函数,返回 0/null。 当 ccc 库不可用时,这是必需的。
如果导入 CCC 库,用户必须删除此文件。

LOOK at this project

执行 IOSLibraries 并查看日志。 在第一次执行时,您会在日志中看到

CCC not found   <--- this line is printed by libstatic (your library)

如果你进入 optional.c 文件并注释 cccfunction(),你会在日志中看到

Executing a function of CCC  <--- this line is printed by libccc
CCC has been found and the function has been executed  <--- this line is printed by libstatic (your library)

如果您同时删除 ccc 库和 optional.c 文件,您将看到

架构 xxxxxx 的未定义符号: “_cccfunction”,引用自: libstaticfirst_universal.a中的_wrapper_cccfunction(wrapper_cccfunction.o)

这就是您需要发布 optional.c 文件的原因,因此用户编译器不会抱怨找不到方法。 当用户拥有 CCC 库时,他可以简单地删除或注释 optional.c 文件。 在您的库中,您将能够通过查看某些控制函数的返回值来测试 CCC 库是否存在

编辑 - 旧答案:在意识到您在 iOS 上之后,以下(也是第一个)答案变得无效。动态链接仅适用于 OSX。但是,我将旧答案留给使用 OSX 的人

老答案
我认为

我假设 CCC 是一个静态库(如果它是动态的则更简单)。在这种情况下,AFAIK,你不能“自动”做任何事情,但是一个很好的折衷办法可以是这样的,使用动态库

用户项目 --include--> 你的静态库 --include--> 动态库 --can include--> CCC 库

创建两个版本的动态库:

  • 实现,例如,CCC 库的空函数 -> 当您调用该函数时,它们返回 0/null 并且您知道该库未实现。你甚至可以使用更智能的东西(一个简单的控制功能)

  • 给用户第二个动态库的源代码,他们只需在项目内拖放CCC库,然后将编译好的库移动到正确的位置即可编译。这不是您的库的源代码(您的代码是在静态部分编译的),而只是您从静态库中调用的包装函数的代码。

  • 您的静态库不直接调用 CCC 库的函数,而只调用始终存在的包装函数(在“空动态库”和“由用户编译的动态库”中)

通过这样做,用户可以将“空”动态库替换为包含 CCC 的动态库。 如果动态库是链接CCC的,最终工程会使用CCC的功能,否则不使用。

Look at the attached example:

  • LibTests 项目实现 lib libstaticlib.a 并调用其函数“usedynamic(int)”
  • libstaticlib.a 实现动态库 libdynamic1 并调用其函数“firstfunction(int)”
  • libdynamic1 有两个不同的副本:一个有一个 firstfunction() 返回传递的数字,另一个返回数字*2

现在,打开 LibTests(应该是您用户的项目),复制 /usr/local/lib/ 中两个已编译动态库中的第一个,然后执行 LibTests:您将在控制台中看到“10”。 现在,将动态库更改为第二个,您将看到“20”。

这就是用户必须做的事情:您出售带有动态“空”组件的库。如果用户购买了CCC,你给出了如何编译绑定CCC的动态组件的说明和代码。动态库构建完成后,用户只需切换.dylib文件

【讨论】:

  • 我想补充一点,如果你的包含顺序是正确的,你不需要删除文件,包含保护应该阻止它被编译和链接。
  • STRUCTS 怎么样,有等价的吗?
  • 结构是您提供给编译器的“简单”指令,您只需将您/用户需要的所有 CCC 结构包含在自定义头文件中,您就可以编译。
  • 如果您提供代码示例,我会给您加分。然而,汤米的回答最终变得更简单了。
【解决方案2】:

所以这是你的问题的要点......

您自己的进程无法交换静态库...即在链接时我链接到 libfoo.1.a 现在在运行时此进程无法可靠地交换 libfoo.2 的符号。一个

所以你需要绕过这个限制。

最简单的方法是使用动态库和动态链接器...但您使用的是 iOS,因此您无权访问它。

如果您可以运行帮助程序,您可能会在第一个进程中更改实际对象,但您使用的是 iOS,这将无法正常工作...

这样就可以让一个对象修改它自己的内容了......代码签名不允许你这样做......

这样就会在你的程序中构建溢出并试图让它执行:)

其实比那个简单多了……

  1. 做一个缓冲区
  2. 用代码片段填充它
  3. 设置 uo 堆栈帧(需要一点 asm)
  4. 为您计划调用的函数设置参数
  5. 运行缓冲区 + 偏移到您的方法
  6. 利润

作为旁注,我写了一个小东西来演示运行时的动态绑定......但你需要有一个编译器等......这个策略在 iOS 上不起作用

https://github.com/gradyplayer/cfeedback

编辑我实际上重新阅读了您的问题,而且我认为您正在尝试解决的问题要容易得多...

您可以使用其他标头的#def'ed 来进行条件编译... 如果有些地方你必须将这些结构之一包含到一个对象中,你可以只对结构进行类型定义,然后只使用指向它的指针,只要库具有构造和销毁功能。

【讨论】:

  • #def 是编译时。正如我所提到的,这些库是预编译的。它需要是运行时的。
  • 这不是因为您可以控制在客户编译时引用的符号,并包含对桥代码的引用(如果定义了某些符号)。
【解决方案3】:

使用dlsym 按函数名获取C 函数指针。如果它能找到它们,它们就在那里。否则他们不是。只需使用RTLD_DEFAULT 作为第一个参数。

编辑:已经对 iOS 示例进行了转换,请参阅 Mike Ash 的 write up of PLWeakCompatibility,尤其是“Falling Through”部分。您会看到他检查是否存在objc_loadWeakRetained(与弱引用相关的运行时调用)。 5+以下是,他的版本直接调用真实的。在 4 岁以下不是,所以他的版本会做其他事情。

EDIT2:示例代码:

样本 1:

#import <Foundation/Foundation.h>
#include <dlfcn.h>

int main(int argc, char *argv[]) 
{
    @autoreleasepool
    {
        NSLog(@"%p", dlsym(RTLD_DEFAULT, "someFunc"));
    }
}

输出0x0。示例 2:

#import <Foundation/Foundation.h>
#include <dlfcn.h>

void someFunc()
{

}

int main(int argc, char *argv[])
{
    @autoreleasepool
    {
        NSLog(@"%p", dlsym(RTLD_DEFAULT, "someFunc"));
    }
}

输出 0x0 以外的地址。

示例 3:

#import <Foundation/Foundation.h>
#include <dlfcn.h>

void someFunc()
{
    NSLog(@"Hi!");
}

int main(int argc, char *argv[])
{
    @autoreleasepool
    {
        void (* func)();
        func = dlsym(RTLD_DEFAULT, "someFunc");
        func();
    }
}

输出Hi!。

结构在运行时不会出现在 .a 或其他地方,它们只是向编译器提供有关如何格式化数据的指令。因此,您需要在代码中包含实际结构或它们的兼容重述。

【讨论】:

  • Afaik dlsym 不适用于静态库中包含的函数。我试过了,返回值总是空的。如果函数在动态库中或在同一个项目中,它可以正常工作
  • 您是否确保禁用了死代码剥离?我想最快的检查方法是:如果您包含对库函数的标准调用,dlsym 会找到它吗?请参阅stackoverflow.com/questions/16294842/… 了解如何禁用一般。 .a 只是 .os 的存档,没有特殊的链接规则。
  • 如果我在项目中包含一个静态库,然后通过dlsym从主项目中搜索该静态库的一个函数,它就可以工作(关闭死代码剥离)。但是,如果我(从主项目)调用库 A 中的一个函数,该函数通过 dlsym 检查库 B 中是否存在一个函数(OP 的情况),那么它就不起作用。我试图理解为什么......
  • 我的错,这是一个愚蠢的问题......即使在这种情况下它也有效 :-) 很好的解决方案。
  • dlfcn.h 是要下载的外部库还是与 iOS SDK 捆绑在一起?
【解决方案4】:

这很棘手,但易于管理。如果您只需要来自 CCC 的 Objective-C 类,这会更容易,但您明确表示您需要访问结构/函数。

  1. 围绕所有 CCC 功能构建一个代理类。所有 CCC 功能都必须封装到代理的实例方法中。所有 CCC 类型都必须适应您自己的类型。 CCC 的任何部分都不能包含在代理类的实现文件之外的任何内容中。我会打电话给这个班MyCCCProxy。

  2. 永远不要直接引用MyCCCProxy 类对象。 稍后会详细介绍。

  3. 在不链接MyCCCProxy.m的情况下构建您的库

  4. 仅使用 MyCCCProxy 构建第二个静态库。

  5. 拥有 CCC 的客户需要链接 AAA、CCC 和 CCCProxy。没有CCC的客户只会链接AAA。

棘手的步骤是第 2 步。

大多数时候,当您创建一个类的实例时,您会使用:

MyCCCProxy *aCCCProxy = [[MyCCCProxy alloc] init];

这直接引用了 MyCCCProxy 的类对象,如果不包含 MyCCCProxy 将导致用户链接问题。

如果你改为写:

MyCCCProxy *aCCCProxy = [[NSClassFromString(@"MyCCCProxy") alloc] init];

这不直接引用类对象,它动态加载类对象。如果MyCCCProxy 不作为类存在,则NSClassFromString 返回Nil(nil 的类版本)。 [Nil alloc] 返回nil。 [nil init] 返回nil。

MyCCCProxy *aCCCProxy = [[NSClassFromString(@"MyCCCProxy") alloc] init];
if (aCCCProxy != nil) {
    // I have access to CCC through MyCCCProxy.
}

【讨论】:

  • 第 4 步 - 构建 MyCCCProxy。既然MyCCCProxy使用的是CCC,那不包括CCC怎么编译呢??
  • 如果MyCCCProxy是静态库,那么结构体的声明只需要CCC的头文件,不需要链接CCC。
  • 如果你想把它推得更远,那么你可以只提供 MyCCCProxy 作为源,客户需要将其包含在他们的项目中。
【解决方案5】:

它不完全是运行时,但可能会根据 CCC 许可证解决您的问题。

选项 1(编译时间)

使用 #ifdef 创建一个 CCC_wrap 库,并给出使用和不使用 CCC_library 编译它的说明。

对于每个 CCC_function,你必须有一个等效的 CCC_function_wrap

如果HAVE_CCC == 1包装函数应该调用CCC库,否则什么都不做或返回错误。

创建一个额外的函数来发现你的库是如何编译的

int CCC_wrap_isfake(void) {
#if HAVE_CCC
    return 0;
#else
    return 1;
#endif
}

选项 2(二进制就绪)

创建两个新库,CCC_wrap 和 CCC_wrap_fake

两个库都必须包含运行程序所需的所有函数/类,但假库中的所有函数都不会做任何事情,只是return 0;

比你创建一个额外的函数CCC_wrap_isfake

CCC_wrap_fake:

int CCC_wrap_isfake(void) { return 1;}

CCC_wrap:

int CCC_wrap_isfake(void) { return 0;}

现在您知道您的代码是使用真正的包装还是假包装运行。

在编译时,您需要设置一个标志来确定您的库将如何链接到您的客户端软件

CCC_wrap_fake:

LDFLGAS=-lCCC_wrap_fake

CCC_wrap:

LDFLGAS=-lCCC_wrap -lCCC

两个选项都应该正确链接。

关于许可要求

如果您提供 CCC_wrap 库源,您的客户端将能够更新 CCC 库,而无需访问您的主源。

在这两种情况下,您都不需要将 CCC 库与您的源代码一起提供。

【讨论】:

    【解决方案6】:

    您的问题在编译时更容易解决,因为您的客户已经需要自己链接所有内容。

    由于您的客户端应该将所有“AAA”代码与“CCC”代码静态链接在一起,因此可以通过指示您的客户端将“AAA.a”与“AAA_with_CCC_glue.a”一起编译来解决您的问题,如果他们如果没有,则有“CCC.a”或“AAA_without_CCC_glue.a”。 _glue.a 都将实现 可能 使用 CCC.a 的一组函数,区别在于它们是否实际使用它。

    要在运行时解决这个问题,您至少需要能够调用dlsym()(这个post 让我认为是的,你可以,但它已经过时了)。尝试在应用程序自己的内存中查找您关心的所有 CCC.a 函数。

    【讨论】:

      猜你喜欢
      • 2014-09-13
      • 2012-12-06
      • 2011-07-16
      • 2013-07-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多