问题 在Objective-C中在运行时检测并使用可选的外部C库


我正在构建一个iPhone开发人员可以在他们的项目中包含的SDK。它以编译的“.a”形式提供,没有源代码。我们称我的SDK为“AAA”。

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

我的SDK,AAA,可以选择使用CCC来改进产品,使用这些第三方功能。例如,假设CCC是用于加密某些内容的安全SDK。 AAA不需要CCC,但如果客户选择在其项目中包含CCC,则会更加安全。

现在这里有一个特别棘手的部分 - CCC库,是纯C代码,由C Structs和C函数组成 - 没有任何面向对象的东西。

问题是:

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

7667
2018-04-16 07:47


起源

通常库定义了一些标志,通知每个人它的礼物。看看,也许CCC呢?然后你可以使用#ifdef / #elif /#endif预处理器命令 - mas'an
这个 可能会有所帮助。 - mas'an
@mas'an我想到了这一点,他们确实定义了#def - 但#ifdef是COMPILE-TIME功能。我正在向客户提供预编译的代码...... - Nathan H
你能创建第三个库来包装CCC库吗? - Jonatan Goebel


答案:


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

编辑:为了一个iOS示例,请参阅Mike Ash的 写下PLWeakCompatibility,特别是“落地”一节。你会看到他检查是否 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或其他地方,它们只是指示编译器如何格式化数据。因此,您需要在代码中包含实际的结构或兼容的重述。


7
2018-04-18 15:33



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


你可以使用弱函数来完成它。 在静态库中,声明要使用的ccc的所有功能,如下所示:

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

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

看看这个项目

执行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 lib和optional.c文件,你会看到

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

这就是您需要发送optional.c文件的原因,因此用户编译器不会抱怨找不到找到的方法。 当用户拥有CCC lib时,他只需删除或注释optional.c文件即可。 在您的库中,您将能够测试CCC库的存在,查看某些控制功能的返回值

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

老答复
我觉得

我假设CCC是一个静态库(如果它是动态的,它更简单)。在这种情况下,AFAIK,你可以“自动地”做任何事情,但是使用动态库可以做出类似的妥协。

用户项目--include - >您的静态库--include - >动态库 - 可以包含 - > CCC库

创建两个版本的动态库:

  • 例如,实现CCC库的空函数 - >当你调用函数时,它们返回0 / null,你知道库没有实现。你甚至可以使用更聪明的东西(简单的控制功能)

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

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

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

看看附带的例子:

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

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

这是用户必须做的事情:您使用动态“空”组件销售库。如果用户购买了CCC,您将提供有关如何使用与之捆绑的CCC编译动态组件的说明和代码。构建动态库之后,用户只需切换.dylib文件即可


3
2018-04-18 14:50



我想补充一点,如果你的包含顺序是正确的,你将不需要删除该文件,包括警卫应该防止它被编译和链接它。 - Jonathan Howard
怎么样STRUCTS,有没有相同的? - Nathan H
CCC lib的函数具有自定义返回类型 struct... - Nathan H
结构是你给编译器的“简单”指令,你只需要包含你/用户在自定义头文件中需要的所有CCC结构,你就可以编译。 - LombaX
我会给你提供代码样本的奖励积分。然而,汤米的回答最终变得更加简单。 - Nathan H


这很棘手,但可以管理。如果您只需要来自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.
}

1
2018-04-18 16:24



第4步 - 构建MyCCCProxy。由于MyCCCProxy使用CCC,如何在不包含CCC的情况下编译它? - Nathan H
如果MyCCCProxy是一个静态库,那么你只需要CCC的头来表示结构的声明,你不需要链接CCC。 - Jeffery Thomas
如果你想将它推得更远,那么你可以只提供MyCCCProxy作为客户需要包含在他们项目中的来源。 - Jeffery Thomas


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

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

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

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

如果你可以运行一个帮助器,你可能会在第一个进程中更改实际的对象,但是你在iOS上,那将无法工作......

所以叶子试图让一个对象修改自己的内容......哪些代码签名不会让你做...

所以留下你的程序溢出并试图让它执行:)

实际上它比那简单得多......

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

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

https://github.com/gradyplayer/cfeedback

编辑 我实际上重新阅读了你的问题,这是一个我认为你试图解决的更容易的问题......

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


0
2018-04-18 15:17



#def是编译时。正如我所提到的,这些库是预先编译的。它需要是运行时。 - Nathan H
它不是因为您可以控制在客户编译时引用的符号,并且如果定义了某个符号,则包括对桥代码的引用。 - Grady Player


它不完全是运行时,但可以根据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库与源代码一起发送。


0
2018-04-22 18:59





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

由于您的客户端应该将所有“AAA”代码与“CCC”代码静态链接,因此可以通过指示客户端一起编译来解决您的问题“AAA.a“要么”AAA_with_CCC_glue.a“如果他们有”CCC.a“ 要么 ”AAA_without_CCC_glue.a“如果他们不这样做。两者都有 _glue.a 将实现那些功能集 可能 使用 CCC.a,区别在于他们是否实际使用它。

要在运行时解决此问题,您至少需要能够调用 dlsym() (这个 岗位 让我觉得是的,你可以,但它已经老了)。试着去寻找所有 CCC.a 您在应用程序内部关注的功能拥有内存。


0
2018-04-23 06:23