问题 在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
起源
答案:
使用 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
你可以使用弱函数来完成它。
在静态库中,声明要使用的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
这很棘手,但可以管理。如果您只需要来自CCC的Objective-C类,这将更容易,但您明确表示您需要访问结构/函数。
围绕所有CCC功能构建代理类。必须将所有CCC功能封装到代理的实例方法中。所有CCC类型必须适应您自己的类型。 CCC的任何部分都不能包含在代理类的实现文件之外的任何内容中。我会打电话给这堂课 MyCCCProxy
。
永远不要直接参考 MyCCCProxy
类对象。 稍后会详细介绍。
无需链接即可构建您的库 MyCCCProxy.m
仅使用MyCCCProxy构建第二个静态库。
拥有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
所以这是你问题的要点......
您自己的进程无法交换静态库...这是在链接时我链接到libfoo.1.a现在在运行时此进程无法可靠地交换libfoo.2.a的符号
所以你需要绕过这个限制。
最简单的是使用动态库和动态链接器......但是您使用的是iOS,因此您无法访问它。
如果你可以运行一个帮助器,你可能会在第一个进程中更改实际的对象,但是你在iOS上,那将无法工作......
所以叶子试图让一个对象修改自己的内容......哪些代码签名不会让你做...
所以留下你的程序溢出并试图让它执行:)
实际上它比那简单得多......
- 制作缓冲区
- 用代码片段填充它
- 设置uo堆栈框架(需要一点asm)
- 为您计划调用的函数设置参数
- 运行缓冲区+偏移到您的方法
- 利润
作为旁注,我写了一个在运行时演示动态绑定的小东西......但是你需要一个编译器等......这个策略在iOS上不起作用
https://github.com/gradyplayer/cfeedback
编辑 我实际上重新阅读了你的问题,这是一个我认为你试图解决的更容易的问题......
您可以使用其他标题#def'ed进行条件编译...
如果有一些地方你必须将这些结构中的一个包含到一个对象中,你只需键入该结构然后只使用它的指针,只要该库具有构造和销毁功能。
0
2018-04-18 15:17
它不完全是运行时,但可以根据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