Hello Haskell Friends,
GHC
always passes -dead_strip_dylibs to the linker on macOS. This means that Haskell programs that use Objective-C-style dynamic binding (via objc_getClass or similar) won't actually be able to find the Objective-C methods they need at runtime. Here's an example illustrating the problem: Consider this small example program:
#include <stdio.h>
extern void *objc_getClass(char *n);
void test_get_class(char *n)
{
void *cp = objc_getClass(n);
if(cp == NULL)
{
printf("Didn't find class %s\n", n);
}
else
{
printf("Found class %s\n", n);
}
}
int main(int argc, char *argv[])
{
test_get_class(argv[1]);
return 0;
}
Building like this:
clang -o hasclass main.c -lobjc -L/usr/lib -framework Foundation -F /System/Library/Frameworks/
Yields an executable that works like this:
$ ./hasclass NSObject
Found class NSObject
$ ./hasclass NSString
Found class NSString
$ ./hasclass NSDate
Found class NSDate
otool shows that we're linked against Foundation properly:
hasclass:
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 1452.23.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
Now consider this equivalent Haskell example:
module Main where
import Foreign.C.String
import Foreign.Ptr
import System.Environment
foreign import ccall objc_getClass :: CString -> IO (Ptr a)
testGetClass :: String -> IO ()
testGetClass n = withCString n $ \cn -> do
cp <- objc_getClass cn
let m | cp == nullPtr = "Didn't find class " ++ n
| otherwise = "Found class " ++ n
putStrLn m
main :: IO ()
main = getArgs >>= (testGetClass . head)
Building like this:
ghc -o hasclass Main.hs -lobjc -L/usr/lib -framework foundation -framework-path /System/Library/Frameworks/
Yields an executable that works like this:
$ ./hasclass NSObject
Found class NSObject
$ ./hasclass NSString
Didn't find class NSString
$ ./hasclass NSDate
Didn't find class NSDate
otool shows that our load commands for Foundation are missing:
hasclass:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/nix/store/7jdxjpy1p5ynl9qrr3ymx01973a1abf6-gmp-6.1.2/lib/libgmp.10.dylib (compatibility version 14.0.0, current version 14.2.0)
/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)
Interestingly, the testGetClass function will work just fine in GHCi, since it always loads all of the shared objects and frameworks it's asked to. As far as I can tell the only way to get a hasclass executable with the correct behavior is to do the final linking manually with Clang.
My understanding is that this behavior was introduced to work around symbol count limitations introduced in macOS Sierra. It would be nice to wrap the frameworks passed to the linker in some flags that spares them from -dead_strip_dylibs. I haven't found such a feature in my limited digging around, but perhaps someone who knows more about systems programming on macOS will have an idea. Statically linking against the system frameworks would be a workable stopgap solution, but I have yet to find an easy way to do that.
I'm curious what others' thoughts are on this issue; it's very difficult to call Objective-C methods from Haskell (without generating Objective-C) without a fix for this.
Regards,
Travis Whitaker