2021-01-07 04:54

An idea for primitive arguments support

I want to describe a pretty damn crazy idea which may bring support for primitive arguments. As far as I can see right now, this is the only way to properly support #467, so it is kind of important :)

A quick reminder: the main problem comes on the definition building stage. With the current setup, we invoke a definition method with TyphoonInjectionByRuntimeArgument instances as faux arguments, but this isn't possible for non-object arguments. If we try to pass objects in place of primitive arguments, we run into all sorts of trouble. So, we need a different approach to denote our runtime arguments.

Here is how it can be done. No matter what primitive type is, we have at least one byte of data at our disposal. The main idea is to use this byte to store our runtime argument index: 0x01 for the first runtime argument, 0x02 for the second, and so on. For object arguments, we would pass NSNumbers containing a value constructed in the same fashion. These values get passed to definition method & property descriptors, where we can convert them back to runtime injections.

However, there is a second trick to this. You might have guessed the problem already: how do we differentiate between disguised runtime injections and actual integers / chars / NSNumbers / NSValues / etc.? Here's how. As we face a possible ambiguity, we should reinvoke the same definition method again, this time encoding runtime arguments with different values (say, shifted by 0x80). We will be able to see what changed and what did not, and therefore figure out the places where we came across genuine values.

I can't see any particular problems with this, but my knowledge of code is still limited, do you?

There is also a way to support TyphoonConfig() in a similar fashion. TyphoonConfig() should basically return NSValue (either directly or, better, masked behind a protocol) and maintain a private global table of mapping "integer values -> config keys". We have 128 spare values, let's say we spend 20 of them to encode runtime arguments, then the rest can go to support config keys and potential other needs - it's a plenty of space.

Sorry if all this is chaotic, I'm pretty excited about this solution and would really love to bring it to life :) Need a word from you guys first though.


  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享
  • 邀请回答


  • weixin_39528000 weixin_39528000 4月前

    Hm, that sounds interesting. some points from me:

    1) We should do this algorithm only with primitive runtime arguments. In other cases let's use old approach with TyphoonInjectionByRuntimeArgument objects 2) Sending argument index as runtime argument value - yes, I though about that too.. but I found that inconvenient in past.. Your suggestion with two calls sounds interesting. I'll think what else we can do here to avoid double Definition creation.

    点赞 评论 复制链接分享
  • weixin_39516956 weixin_39516956 4月前

    1) We should do this algorithm only with primitive runtime arguments. In other cases let's use old approach with TyphoonInjectionByRuntimeArgument objects

    The NSNumber instead of TyphoonInjectionByRuntimeArgument approach works well because basically it allows for an easier, unified logic. With primitive arguments, we'll retrieve them as boxed NSValues, so we can use the same injection transformation code for both objects and primitives.

    It also gives us a direct support for the new API from #467, e.g.:

    - (id)knightWithDamselsRescued:(NSNumber *)damselsRescued
        return [[Knight typhoonDefinition] initWithDamselsRescued:damselsRescued.unsignedIntegerValue];

    Note damselsRescued.unsignedIntegerValue here, it won't work with TyphoonInjectionByRuntimeArgument.

    点赞 评论 复制链接分享
  • weixin_39516956 weixin_39516956 4月前

    I've finally got a working implementation of this :) Wanna clean up a bit before opening a PR, but it works and passes all the tests. The only thing it lacks at the moment is support for BOOLs (@(boolRuntimeArgument) in particular), but I have a plan for that too.

    Also, I've done some profiling of our project which uses the old version of Typhoon. Apparently, the definition building part doesn't take a big percentage of Typhoon startup - e.g., it takes less than assembly selector swizzling or actual singleton instantiation. So, having to call each definition method twice might not be such a big of deal. Gonna do some profiling with the new version soon.

    点赞 评论 复制链接分享
  • weixin_39556590 weixin_39556590 4月前

    Wow congrats, looking forward to that!

    点赞 评论 复制链接分享
  • weixin_39516956 weixin_39516956 4月前

    After some further consideration, I guess I came to a conclusion that this is probably not a good idea after all. While it's possible to work around sharp corners of BOOLs, the whole process of encoding/decoding turned out to be an unnecessarily complex routine. In fact, it introduces not-so-obvious problems on the user level as well. Let's say we have this:

    - (LittleEngine *)littleEngineThatCould:(BOOL)could {
        return [TyphoonDefinition withClass:[LittleEngine class] configuration:^(TyphoonDefinition *definition) {
            [definition injectProperty:(failed) with:@(!could)];

    There's nothing wrong with that, but it won't work, and it's very confusing why it wouldn't. As a user, I'd have to keep in mind that I have to use my primitive arguments as is, and if I forget about that, there's no way to give an early warning. This is too bad.

    However, all this actually brings us to a new approach I would like to discuss in #467 (as a more on-topic issue).

    点赞 评论 复制链接分享
  • weixin_39943678 weixin_39943678 4月前

    The discussion was moved to PR #467.

    点赞 评论 复制链接分享