2

I have a project that has a library subproject that gets imported. I have access to the source code of both the main project and the subproject.

The subproject uses Core Text. Because the subproject must be used on pre and post 3.2 applications, Core Text is weak linked and all of the Core Text related code is wrapped in the logic:

#if defined(__CORETEXT__) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_3_2

The thought of this line of code is that if CoreText is not linked in, the code is skipped. If the iPhone version is less than 3.2; CoreText is not linked in.

The reason for this goal is that the main project(s) (and there are several) do not all use Core Text and if the don't then without the 'defined(CORETEXT)`, they will not compile.

This seems to work fine and everything compiles without an error on projects that use Core Text. However on execution, the code is not found and a runtime error along the lines of "NSString does not respond to XXXX" (part of the code is a category on NSString).

Has anyone run into this? Is my question, partially vague due it being client work, clear?

Ideally I want to set this up so that the subproject does not need to be altered when the main project uses Core Text and when it does not.

Note that __CORETEXT__ is defined in the header of the Core Text framework.

Update on the issue

I have tried the suggestions offered so far and they are ineffectual. Perhaps a little more code will help to outline the issue. I have a category with the following header:

@interface NSString (Internal)

- (NSString *)stringByUnescapingEntities;
- (NSString *)flattenHTML;
- (NSString *)flattenHTMLAndParseParagraphBreaks:(BOOL)parseBreaks;
- (NSString *)stringByEscapingForURL;
- (NSString *)stringByEscapingForJSON;

#if defined(__CORETEXT__) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_3_2
- (CFAttributedStringRef)attributedStringFromHTML;
- (CFAttributedStringRef)attributedStringFromHTMLWithBaseAttributes:(NSDictionary*)baseAttributes;
#endif

@end

Positive Test

All of the methods outside of the #if block work perfectly. All of the methods inside of the #if compile perfectly, no warnings.

Negative Test

If I change __CORETEXT__ to something like __THIS_IS_NOT_REAL__ I will get a compile error for the methods inside of the block. Therefore I know, at compile time at least, that the __CORETEXT__ flag is defined and fully functional.

Runtime issue

However, when I go to access one of the methods that are declared inside of the #if block, I get the following error at runtime:

-[NSCFString attributedStringFromHTMLWithBaseAttributes:]: unrecognized selector sent to instance 0x689c000

Instance 0x689c000 being a NSString.

If I remove the __CORETEXT__ logic check then everything works so long as the master project is using Core Text. If the master project is not using Core Text then compile errors about missing linkers shows up.

Hopefully that clarifies the issue.

NOTE: All projects are using the -all_load flag; as it was needed for TouchXML already.

I have created a test project that demonstrates the issue.

Test Project

Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • Do you by chance wrap the implementations with the same #if statement? Also, it's a little unclear from your post whether if this error only occurs on devices without CoreText, or on all devices (regardless of whether they actually have CoreText). – David Liu Aug 27 '10 at 21:00
  • The implementation is wrapped in the same code. Presently the error is occurring only on projects that attempt to access the `-attributedStringFromHTMLWithBaseAttribtues:` method and are using Core Text. – Marcus S. Zarra Aug 29 '10 at 15:58

7 Answers7

2

The fact that you call out that at least one thing that ends up missing is a category method for NSString makes me think you're running into a lovely issue that seems to be a possible bug in the linker.

Firstly, see this answer elsewhere and ensure that the project including your library is using the -all_load linker flag.

Second, in some dealings I've had with using the Three20 library for the iPhone it became necessary to also use this weird "hack" that applies if, for example, your NSString category is the only thing in the .m file where it's implemented.

//CategoryBugHack.h
#define FIX_CATEGORY_BUG(name) @interface FIX_CATEGORY_BUG##name @end @implementation FIX_CATEGORY_BUG##name @end

Then the category .h/.m ...

//NSStringCategory.h
@interface NSString (MyAdditions)
// etc, etc
@end

//NSStringCategory.m
#import "CategoryBugHack.h"
FIX_CATEGORY_BUG(NSString_MyAdditions)
@implementation NSString (MyAdditions)
// etc, etc
@end

Basically there seems to be an additional issue related to static libraries where an implementation file that contains ONLY implementations of category methods. The FIX_CATEGORY_BUG macro basically just expands to define an interface and implementation of that interface with no methods, just to force something into the .m file that isn't a category.

Community
  • 1
  • 1
imaginaryboy
  • 5,979
  • 1
  • 32
  • 27
  • Updated the question. `-all_load` is already used and other methods outside of the `#if` block are working perfectly without the macro you suggested. – Marcus S. Zarra Aug 27 '10 at 16:46
1

You could do [NSObject respondsToSelector:] checks to see if the category method is available or not.

Edit: I think I didn't fully read the question the first time around. Here are a couple more things that I saw.

That #if probably won't work. This is because this is all checked at compile-time rather than run-time. I'm not exactly sure on what gets included in when weak-linking, but as I understand it, the header of the weak-linked framework will get processed at compile-time. That means the symbols for the method prototypes will be available when you build, which is why there are no build errors or warnings.

I'm also 99% sure that the iOS check will not work. That only checks the max version allowed, which is set at compile-time. Unless you're making different builds with different settings for each iOS (which I assume not), it's pretty much always going to be the same value regardless of which device you're running on.

You're definitely going to need runtime checks (e.g. the afore mentioned respondsToSelector). Version checking of the iOS device can only be done at runtime as well.

David Liu
  • 9,426
  • 5
  • 40
  • 63
  • Very good point. The issue I am running into is that it is not available in the main project when it should be. It compiles without warnings or errors (and I did a negative check to confirm) but at runtime it is failing. – Marcus S. Zarra Aug 26 '10 at 20:40
  • See the new edits for info. I'm not sure what you mean by negative check though. Could you clarify? – David Liu Aug 26 '10 at 21:22
0

Maybe you should make the conditional into

#if defined(__CORETEXT__) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_3_2)

After all, the rest of the referenced project is linked in, correct? The problem must be the preprocessor's conditional.

Huperniketes
  • 940
  • 7
  • 17
  • if I remove the `defined(__CORETEXT__)` portion it works fine in post 3.2 but then fails in projects that are not using Core Text. – Marcus S. Zarra Aug 28 '10 at 17:26
  • Don't remove defined(__CORETEXT__) portion, just put parentheses around __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_3_2. They're needed because && has greater precedence than >=, even in the preprocessor. – Huperniketes Aug 30 '10 at 00:45
  • Confounding markdown. And confounding SO for not indicating the formatting rules for comments. – Huperniketes Aug 30 '10 at 00:59
0

I remember I did something similar to David but just for a single method. I used Objective-C runtime functions to call that method so it will compile in every version. I did something like:

if ([theReceiver respondToSelector:@selector(mySelector:)]){
   objc_msgSend(theReceiver, @selector(mySelector:), param1);
}else{
   // do it other way here.
}

Not the most beautiful code I have written, nor I have tried before but maybe is possible to add your methods and ivars with runtime functions like class_addIvar, class_addMethod, etc.

nacho4d
  • 43,720
  • 45
  • 157
  • 240
0

If the category is housed in the sub project, you may need to set the target of that project. I have had weird side effects like this when doing sub projects within a project.

Edit: Meaning, the subproject could be set to compile against 3.1.3 and possible never meets your logic?

bstahlhood
  • 2,006
  • 13
  • 10
0

I believe I found a similar situation recently, and as I told you on twitter I think you need to set -ObjC in addition to -all_load, as described here: http://developer.apple.com/mac/library/qa/qa2006/qa1490.html

pcuenca
  • 91
  • 3
0

An answer in two parts:

  • CoreText.h is imported in your app's PCH file, but nowhere in the library (PCH's do not cascade). Therefore, in the library the __CORETEXT__ code is always #ifdef'd out. However, when the exported header from the library is compiled in the App, the #ifdef'd method prototype is included, and therefore you get no compile-time warnings for that method on NSString.

    A quick fix for this is putting a literal

    #import <CoreText/CoreText.h>
    

    in your library's PCH file. Which of course, defeats the purpose of what you are trying to do.

    Fixing that is another subject, but I suspect you would be better off using your own -D or #define, and then trying to cascade that your subproject, though I don't know how to do that myself, yet.

  • One good way to temporarily test if your #ifdef'd code is compiling is putting #error's in the conditionals, e.g.

    #ifdef WHEE
         // compile-time error will happen here
         #error
    #endif
    
Clay Bridges
  • 11,602
  • 10
  • 68
  • 118