How to resolve symbol name conflict in a Cocoa touch framework

前端 未结 4 1057
死守一世寂寞
死守一世寂寞 2021-01-01 20:10

I developed a Cocoa touch framework and am having problems with third party static framework classes which are embedded inside of it.

The problem is symbol collision

4条回答
  •  迷失自我
    2021-01-01 20:35

    TL;DR

    Using dynamic frameworks, you should not have to care much about it, as the linker uses a sensible default behaviour. If you really want to do what you ask, you could instruct the linker to do so, at the risk of failure at run-time. See towards the end of the answer for an explanation.

    In general, regarding static linking

    This is another version of the classic "dependency hell" problem. Theoretically, there are two solutions when linking object files statically:

    1. State your dependency and let the user of your framework solve it in their build system. Some ideas:

      • External systems such as CocoaPods and Carthage will help you at the downside of forcing constraints onto your user's build system (i.e. the user might not use CocoaPods).

      • Include the dependency and the header files for it, instructing your users not use your provided version of that dependency. The downside is of course that your user cannot switch the implementation should they need to.

      • (Maybe the easiest). Build two versions of your framework, one with the dependency library not linked in. The user can then choose wheter to use your provided version or their own. The downside is that there is no good way of determining if their version will be compatible with your code.

    2. Avoid leaking your dependency and encapsulate it within your framework, at the expense of larger code size. This is usually my path of preference these days now that code size is not a real problem even on mobile devices. Methods include:

      • Include the dependency as source files in your project. Use #define macros to rename the symbols (or using only static symbols).
      • Building your dependency from source, renaming the symbols in a code pre-build (or one off) step.
      • Building everything as you were, then renaming the "internal" symbols in the built library. This could potentially cause bugs, especially since swift/obj-c have dynamic aspects. See this question, for example.

    This post discusses some of the pros and cons of the different alternatives.

    When using dynamic frameworks

    If you are building a dynamic framework, best practice is to do "2." above. Specifically, linking dynamically will prevent the duplicate symbol problem, as your library can link to its version of the third party library regardless of the library any client uses.

    Here is a simple example (using C for simplicity, but should apply to Swift, ObjC, C++ or anything linked using ld):

    Note also that I assume your third party library is written in C/objC/C++, since Swift classes can (AFAIK) not live in static libraries.

    myapp.c

    #include 
    
    void main() {
      printf("Hello from app\n");
    
      third_party_func();
    
      my_dylib_func();
    }
    

    mylib.c

    #include 
    
    void my_dylib_func() {
      printf("Now in dylib\n");
      third_party_func();
      printf("Now exiting dylib\n");
    }
    

    thirdparty_v1.c

    #include 
    
    void third_party_func() {
      printf("Third party func, v1\n");
    }
    

    thirdparty_v2.c

    #include 
    
    void third_party_func() {
      printf("Third party func, v2\n");
    }
    

    Now, let's first compile the files:

    $ clang -c *.c
    $ ls *.o
    myapp.o         mylib.o         thirdparty_v1.o thirdparty_v2.o
    

    Next, generate the static and dynamic libraries

    $ ar -rcs libmystatic.a mylib.o thirdparty_v1.o
    $ ld -dylib mylib.o thirdparty_v1.o -lc -o libmydynamic.dylib
    $ ls libmy* | xargs file
    libmydynamic.dylib: Mach-O 64-bit dynamically linked shared library x86_64
    libmystatic.a:      current ar archive random library
    

    Now, if we compile statically using the (implicitly) provided implementation:

    clang -omyapp myapp.o -L. -lmystatic thirdparty_v2.o && ./myapp
    Hello from app
    Third party func, v1
    Now in dylib
    Third party func, v1
    Now exiting dylib
    

    Now, this was quite surprising to me, as I was expecting a "duplicate symbol" error. It turns out, ld on OSX silently replaces the symbols, causing the user's app to replace the symbols in my library. The reason is documented in the ld manpage:

    ld will only pull .o files out of a static library if needed to resolve some symbol reference

    This would correspond to point 1. above. (On a side note, running the above example on Linux sure gives a "duplicate symbol" error).

    Now, let's link dynamically as in your example:

    clang -omyapp myapp.o -L. -lmydynamic thirdparty_v2.o && ./myapp
    Hello from app
    Third party func, v2
    Now in dylib
    Third party func, v1
    Now exiting dylib
    

    As you can see, your dynamic library now refer to its version (v1) of the static library, while the app itself will use the other (v2) version. This is probably what you want, and this is the default. The reason is of course that there are now two binaries, each with its own set of symbols. If we inspect the .dylib, we can see that it still exports the third party library:

    $ nm libmydynamic.dylib 
    0000000000000ec0 T _my_dylib_func
                     U _printf
    0000000000000f00 T _third_party_func
                     U dyld_stub_binder
    

    And sure, if we don't link to the static library in our app, the linker will find the symbol in the .dylib:

    $ clang -omyapp myapp.o -L. -lmydynamic  && ./myapp
    Hello from app
    Third party func, v1
    Now in dylib
    Third party func, v1
    Now exiting dylib
    

    Now, if we don't want to expose to the app that we use some static library, we could hide it by not exporting its symbols (hide it as in not letting the app accidentally reference it, not hiding it truly):

    $ ld -dylib mylib.o -unexported_symbol '_third_party_*' thirdparty_v1.o -lc -o libmydynamic_no3p.dylib
    $ nm -A libmydyn*
    ...
    libmydynamic.dylib: 0000000000000f00 T _third_party_func
    libmydynamic_no3p.dylib: 0000000000000f00 t _third_party_func 
    

    (A capital T means the symbol is public, a lowercase t means the symbol is private).

    Let's try the last example again:

    $ clang -omyapp myapp.o -L. -lmydynamic_no3p  && ./myapp
    Undefined symbols for architecture x86_64:
      "_third_party_func", referenced from:
          _main in myapp.o
    ld: symbol(s) not found for architecture x86_64
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    

    Now we have successfully hidden our third party static framework to client apps. Note that, again, normally you won't have to care.

    What about if you really want 1. in dynamic frameworks

    For example, your lib might need the exact version of the third party library that your client provides.

    There is a linker flag for that too, of course: -undefined dynamic_lookup.

    $ ld -dylib mylib.o -undefined dynamic -lc -o libmydynamic_undef.dylib
    $ clang -omyapp myapp.o -L. -lmydynamic_undef thirdparty_v2.o && ./myapp
    Hello from app
    Third party func, v2
    Now in dylib
    Third party func, v2
    Now exiting dylib
    

    The downside is of course that it would fail at run-time if your client fails to include the static library.

提交回复
热议问题