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

时间:2014-04-16 07:47:06

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

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

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

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

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

问题是:

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

6 个答案:

答案 0 :(得分:8)

使用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或其他地方,它们只是编译器关于如何格式化数据的指令。因此,您需要在代码中包含实际结构或兼容重述。

答案 1 :(得分:3)

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

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

不要在你的lib中包含ccc。 由于函数声明为弱,编译器不会抱怨它们的缺失,但是你可以在代码中引用它。 然后,当您将库分发给用户时,给它们一个内部有空ccc函数的.c文件,返回0 / null。 当ccc lib不可用时,这是必要的 如果导入了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 lib和optional.c文件,您将看到

架构xxxxxx的未定义符号:   &#34; _cccfunction&#34;,引自:       libstaticfirst_universal.a中的_wrapper_cccfunction(wrapper_cccfunction.o)

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

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

OLD ANSWER
我想那个

我认为CCC是一个静态库(如果它是动态的,它更简单)。在这种情况下,AFAIK,你不能做任何事情&#34;自动&#34;,但是使用动态库可以做出类似的妥协

用户项目--include - &gt;你的静态库--include - &gt;动态库 - 可以包括 - &gt; CCC图书馆

创建两个版本的动态库:

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

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

  • 您的静态库不直接调用CCC库的函数,而只调用始终存在的包装函数(在&#34;空动态库&#34;以及&#34;逐个编译的动态库&#34;)

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

Look at the attached example:

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

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

这是用户必须做的事情:你用动态的#34;空的&#34;零件。如果用户购买了CCC,您将提供有关如何使用与之捆绑的CCC编译动态组件的指令和代码。构建动态库之后,用户只需切换.dylib文件

答案 2 :(得分:1)

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

  1. 围绕所有CCC功能构建代理类。必须将所有CCC功能封装到代理的实例方法中。所有CCC类型必须适合您自己的类型。 CCC的任何部分都不能包含在代理类的实现文件之外的任何内容中。我将此课程称为MyCCCProxy

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

  3. 在不链接MyCCCProxy.m

  4. 的情况下构建您的库
  5. 仅使用MyCCCProxy构建第二个静态库。

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

  7. 棘手的一步是2号。

    大多数情况下,当您创建类的实例时,请使用:

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

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

    相反,如果您改为写:

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

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

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

答案 3 :(得分:0)

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

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

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

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

如果您可以运行帮助程序,您可以在第一个进程中更改实际对象,但是您在iOS上并且不能正常工作......

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

这样就可以在程序中构建溢出并尝试让它执行:)

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

  1. 制作缓冲区
  2. 用代码片段填充
  3. 设置堆栈框架(需要一点asm)
  4. 为您打算调用的函数设置参数
  5. 运行缓冲区+偏移到您的方法
  6. 利润
  7. 作为旁注,我写了一个小东西,演示了在运行时的动态绑定......但是你需要一个编译器等......这个策略不能在iOS上运行

    https://github.com/gradyplayer/cfeedback

    编辑我实际上重新阅读了您的问题,而且我认为您试图解决这个问题要轻松得多......

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

答案 4 :(得分:0)

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

答案 5 :(得分:0)

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

由于您的客户应该静态链接您的所有&#34; AAA&#34;代码与&#34; CCC&#34;代码,你的问题可以通过指导你的客户一起编译来解决#34; AAA.a&#34;使用&#34; AAA_with_CCC_glue.a&#34;如果他们有&#34; CCC.a&#34;或&#34; AAA_without_CCC_glue.a&#34;如果他们不这样做。两个_glue.a都会实现可能使用CCC.a的一组函数,区别在于它们是否实际使用它。

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