C function in library code gets stripped when distributed

This is a continuation of https://developer.apple.com/forums/thread/795348

I rambled too much and did not understand the underlaying problem.

The problem is that I have a C function in a iOS library. I want to call this C function from a dylib that this library loads on runtime. When running directly from Xcode (either in debug or release mode) this works correctly. However, when the app is uploaded to testflight or distributed for debugging then the function is stripped and a null function pointer exception crashes the app.

In the last post it was really hard to explain and I was pressed on time but I've created a minimal reproducible example:

https://github.com/ospfranco/dylib_crash

The instructions to run and reproduce the crash are on the README.

Answered by ospfranco in 870868022

After much much tweaking and testing. I finally called the C function from another place inside the library and this did work. It's only when called from the dylib that it fails.

The symbol _say_hello_world was being stripped from the binary's symbol table during the release build.

The function code existed in the binary (verified via dSYM), but Xcode's default Strip Style = All Symbols removes symbol names from the export table. Since dlsym() looks up symbols by name, it couldn't find the function.

In the app's Xcode project (not Package.swift):

Build Settings → Strip Style → Non-Global Symbols

This preserves global symbols (like _say_hello_world) in the binary's symbol table while still stripping local/debug symbols.

Limitation

This setting cannot be controlled from Package.swift. SPM package authors must document that users need to set STRIP_STYLE = non-global in their app's build settings when using dlsym() with statically linked code.

Basically, this is a dead end trying to keep the global symbols alive. The only way I have found to make it work is passing the function pointers directly to my dylib and then there is no symbol resolution necessary.

Here is a more step by step breakdown of the problem

  1. I have a iOS library that exposes a C function. This function is marked with default visibility and used via attribute tags to prevent the compiler/linker from stripping it

    #ifdef __cplusplus
    #define DYLIB_EXPORT                                                           
      extern "C" __attribute__((visibility("default"))) __attribute__((used))
    #else
    #define DYLIB_EXPORT                                                           
      __attribute__((visibility("default"))) __attribute__((used))
    #endif
    
    DYLIB_EXPORT bool say_hello_world();
    
    #import "helper_functions.h"
    #import <Foundation/Foundation.h>
    
    bool say_hello_world() {
      NSLog(@"Hello WORLD 🟢!");
      return true;
    }
    
  2. From a dynamic library, called sdk, I want to call this function. In this example it's a Rust lib

    unsafe extern "C" {
        fn say_hello_world() -> bool;
    }
    
    #[unsafe(no_mangle)]
    extern "C" fn sdk_init() -> i32 {
        unsafe {
            say_hello_world();
        }
        return 0;
    }
    
  3. When the app is compiled and launched from Xcode (either on debug or release schemes) then this works, the following snippet works and prints the hello WORLD message:

     #import "OpacityObjCWrapper.h"
     #import "helper_functions.h"
     #import "sdk.h"
    
     @implementation OpacityObjCWrapper
     // This function is called on the main view controller mount
     + (int)initialize {
       NSBundle *frameworkBundle =
           [NSBundle bundleWithIdentifier:@"sdk"];
       if (![frameworkBundle isLoaded]) {
         BOOL success = [frameworkBundle load];
         if (!success) {
           return -1;
         }
    
         int status = sdk_init();
         return status;
       }
    
       return 0;
     }
    
     @end
    
  4. However, when archived and uploaded to TestFlight an extra stripping step happens that gets rid of the say_hello_world function. The dynamic library then crashes with a null pointer exception:

    Thread 3 name:
    Thread 3 Crashed:
    0   ???                           	0x0000000000000000 0x0 + 0
    1   sdk                           	0x0000000105931ec0 0x105820000 + 1121984
    

Minimum Reproducible Example

Besides the snippets, the only way to reproduce the example is with a fully compilable iOS app one can generate a stripped binary that runs on device. Therefore I have reduced the code as much as a I can to reproduce the issue in this repo:

https://github.com/ospfranco/dylib_crash

Then archive the app, then export with the debugging option. Drag and drop to a connected iPhone finder window to directly install on the device.

Extra notes

  • The Rust library has been compiled with extra flags to prevent the linker from failing when the function is not present during compilation:
    println!("cargo:rustc-link-arg=-Wl,-U,_say_hello_world");
    
  • I know for a fact this is a problem with dead code stripping because if I disable Dead Code Stripping in the project settings then no code is stripped at all, bloating the binary, but the crash does not happen anymore. Yet this only happens when creating a distributable .ipa and not when running directly from Xcode
Accepted Answer

After much much tweaking and testing. I finally called the C function from another place inside the library and this did work. It's only when called from the dylib that it fails.

The symbol _say_hello_world was being stripped from the binary's symbol table during the release build.

The function code existed in the binary (verified via dSYM), but Xcode's default Strip Style = All Symbols removes symbol names from the export table. Since dlsym() looks up symbols by name, it couldn't find the function.

In the app's Xcode project (not Package.swift):

Build Settings → Strip Style → Non-Global Symbols

This preserves global symbols (like _say_hello_world) in the binary's symbol table while still stripping local/debug symbols.

Limitation

This setting cannot be controlled from Package.swift. SPM package authors must document that users need to set STRIP_STYLE = non-global in their app's build settings when using dlsym() with statically linked code.

Basically, this is a dead end trying to keep the global symbols alive. The only way I have found to make it work is passing the function pointers directly to my dylib and then there is no symbol resolution necessary.

C function in library code gets stripped when distributed
 
 
Q