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

我正在构build一个iPhone开发人员可以在他们的项目中包含的SDK。 它以编译的“.a”forms提供,没有源代码。 我们打电话给我的SDK“AAA”。

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

我的SDK(AAA)可以select使用CCC来改进产品,使用这些第三方function。 例如,让我们说CCC是一个安全的SDK来encryption的东西。 AAA不需要CCC,但如果客户select将CCC纳入其项目中,AAA也将更安全。

现在这里是一个额外的棘手的部分 – CCC库,纯C代码,由C结构和C函数 – 没有任何面向对象。

问题是:

  • 我如何编译我的AAA SDK以使用CCC中的函数/结构体,而不在我的项目中包含CCC(未经法律允许,并且不希望跟上版本更新)。
  • 我如何检测客户是否在他的项目中有CCC,只有在可用时才使用这些额外function?

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

编辑:有一个iOS例子,请参阅Mike Ash 写的PLWeakCompatibility ,特别是关于“Falling Through”的部分。 您会看到他检查是否存在objc_loadWeakRetained (与弱引用相关的运行时调用)。 在5+以下和他的版本直接调用真正的版本。 4岁以下,他的版本不是这样做的。

编辑2:示例代码:

样品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或其他地方,只是编译器如何格式化数据的说明。 所以你需要在你的代码中包含实际的结构或者兼容的重述。

你可以使用弱function来做到这一点。 在你的静态库中,声明你想要使用的所有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) 

如果你在可选的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.o)中的_wrapper_ccc函数

这就是为什么你需要发送optional.c文件的原因,所以用户编译器不会抱怨找不到的方法。 当用户拥有CCC库时,他可以简单地删除或注释optional.c文件。 在你的库中,你将能够testingCCC库的存在,查看一些控制函数的返回值

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

老解答
我觉得

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

用户项目 – 包括 – >你的静态库 – 包括 – >一个dynamic库 – 可以包括 – > CCC库

创build两个版本的dynamic库:

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

  • 向用户提供第二个dynamic库的源代码,他们可以简单地通过拖放项目内的CCC库,然后将编译的库移到正确的位置进行编译。 这不是你的库的源代码(你的代码是在静态部分编译的),而只是你从静态库调用的包装函数的代码。

  • 你的静态库不会直接调用CCC库的函数,而只能调用总是存在的包装函数(在“空的dynamic库”和“由用户编译的dynamic库”中)

通过这样做,用户可以用包含CCC的dynamic库replace“空”dynamic库。 如果dynamic库是CCC链接的,则最终项目将使用CCC的function,否则不会。

看下面的例子:

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

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

这是用户必须做的:你用一个dynamic的“空”组件出售这个库。 如果用户购买了CCC,那么就给出了如何编译与其捆绑的CCC的dynamic组件的指令和代码。 dynamic库build立后,用户只需切换.dylib文件

这是棘手的,但易于pipe理。 如果你只需要来自CCC的Objective-C类,这将更容易,但是你特别说你需要访问结构体/函数。

  1. 围绕所有的CCCfunction构build一个代理类。 所有CCCfunction都必须封装到代理的实例方法中。 所有的CCCtypes必须适应你自己的types。 CCC的任何部分都不能包含在代理类的实现文件之外的任何东西中。 我将称这个类为MyCCCProxy

  2. 永远不要直接引用MyCCCProxy类对象。 稍后再说。

  3. 构build你的库,而不用链接MyCCCProxy.m

  4. 用MyCCCProxy构build第二个静态库。

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

棘手的一步是2号。

大多数情况下,当你创build一个类的实例时,你可以使用:

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

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

相反,如果你写:

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

这不直接引用类对象,它dynamic地加载类对象。 如果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. } 

所以这是您的问题的要点…

一个静态库不能被你自己的进程交换…这是链接时,我现在在运行时链接到libfoo.1.a这个进程不能可靠地交换符号为libfoo.2.a

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

最简单的是使用dynamic库和dynamic链接器…但是你使用iOS,所以你没有访问。

如果你可以运行一个帮助程序,你可能会改变第一个进程中的实际对象,但是你在iOS上,这是行不通的。

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

所以叶子build立一个溢出到你的程序,并试图让它执行:)

其实它比这更简单…

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

作为一个侧面说明我写了一个小东西,演示dynamic绑定在运行时…但你需要有一个编译器等…这种策略将无法在iOS

https://github.com/gradyplayer/cfeedback

编辑我其实重新读你的问题,这是一个更容易的,我以为你想解决…

你可以使用其他头文件中定义的任何东西来进行条件编译……如果有必要将这些结构中的一个包含到对象中的地方,则只需键入该结构,然后只使用指向它的指针,只要图书馆具有build设和销毁function。

这不是确切的运行时间,但可以根据CCC许可证解决您的问题。

选项1(编译时间)

使用#ifdef创build一个CCC_wrap库,并给出指令来编译它,不pipe是否有CCC_library。

对于每个CCC_function,您必须具有等效的CCC_function_wrap

如果HAVE_CCC == 1 ,包装函数应该调用CCC库,否则不做任何事情或返回错误。

创build一个额外的函数来发现你的图书馆是如何编译的

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

选项2(二进制准备)

创build两个新库,CCC_wrap和CCC_wrap_fake

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

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

CCC_wrap_fake:

 int CCC_wrap_isfake(void) { return 1;} 

CCC_wrap:

 int CCC_wrap_isfake(void) { return 0;} 

现在你知道你的代码是用真正的wrap还是假的。

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

CCC_wrap_fake:

 LDFLGAS=-lCCC_wrap_fake 

CCC_wrap:

 LDFLGAS=-lCCC_wrap -lCCC 

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

关于许可证要求

如果您提供CCC_wrap库源,则客户端将能够更新CCC库,而不必访问您的主要来源。

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

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

由于您的客户端应该将所有“AAA”代码与“CCC”代码静态链接在一起,因此如果客户有“CCC”代码,可以通过指示客户端将“ AAA.a ”与“ AAA_with_CCC_glue.aCCC.a “或” AAA_without_CCC_glue.a “,如果不是的话。 两个_glue.a都会实现可能使用CCC.a的一组函数,区别在于它们实际使用它。

要在运行时解决这个问题,你至less需要调用dlsym() (这个post让我觉得是的,你可以,但是它是旧的)。 试着去寻找你所关心的所有CCC.a函数。

Interesting Posts