Saturday, June 29, 2024
HomeiOS DevelopmentDynamic libraries and code replacements in Swift

Dynamic libraries and code replacements in Swift

[ad_1]

Easy methods to load a dynamic library and use native technique swizzling in Swift? This text is all concerning the magic behind SwiftUI previews.

Swift

Dynamic library packages

I’ve already printed an article about constructing static and dynamic libraries utilizing the Swift compiler, if you do not know what’s a dynamic library or you’re merely a bit extra about how the Swift compiler works, it is best to undoubtedly check out that submit first.

This time we will focus a bit extra on using the Swift Bundle Supervisor to create our dynamic library merchandise. The setup goes to be similar to the one I’ve created within the loading dynamic libraries at runtime article. First we will create a shared library utilizing SPM.



import PackageDescription

let bundle = Bundle(
    title: "TextUI",
    merchandise: [
        .library(name: "TextUI", type: .dynamic, targets: ["TextUI"]),
    ],
    dependencies: [
        
    ],
    targets: [
        .target(name: "TextUI", swiftSettings: [
            .unsafeFlags(["-emit-module", "-emit-library"])
        ]),
    ]
)


The bundle manifest is sort of easy, though there are a couple of particular issues that we had so as to add. The very very first thing is that we outlined the product kind as a dynamic library. This can be certain that the proper .dylib (or .so / .dll) binary shall be created if you construct the goal. 🎯

The second factor is that we would prefer to emit our Swift module information alongside the library, we will inform this to the compiler via some unsafe flags. Do not be afraid, these are literally not so harmful to make use of, these flags shall be straight handed to the Swift compiler, however that is it.



Now the supply code for our TextUI library goes to be quite simple.


public struct TextUI {

    public static dynamic func construct() -> String {
        "Good day, World!"
    }
}


It is only a struct with one static operate that returns a String worth. Fairly easy, besides one factor: the dynamic key phrase. By including the dynamic modifier to a operate (or technique) you inform the compiler that it ought to use dynamic dispatch to “resolve” the implementation when calling it.

We will make the most of the dynamic dispatch afterward, however earlier than we might transfer onto that half, we now have to construct our dynamic library and make it accessible for others to make use of. 🔨

For those who run swift construct (or run the undertaking through Xcode) it’s going to construct all of the required recordsdata and place them beneath the correct construct folder. You too can print the construct folder by working the swift construct -c launch --show-bin-path (-c launch is for launch builds, we will construct the library utilizing the discharge configuration for apparent causes… we’re releasing them). For those who checklist the contents of the output listing, it is best to discover the next recordsdata there:

  • TextUI.swiftdoc
  • TextUI.swiftmodule
  • TextUI.swiftsourceinfo
  • libTextUI.dylib
  • libTextUI.dylib.dSYM

So, what can we do with this construct folder and the output recordsdata? We will want them beneath a location the place the construct instruments can entry the associated recordsdata, for the sake of simplicity we will put every part into the /usr/native/lib folder utilizing a Makefile.

PRODUCT_NAME := "TextUI"
DEST_DIR := "/usr/native/lib/"
BUILD_DIR := $(shell swift construct -c launch --show-bin-path)

set up: clear
    @swift construct -c launch
    @set up "$(BUILD_DIR)/lib$(PRODUCT_NAME).dylib" $(DEST_DIR)
    @cp -R "$(BUILD_DIR)/lib$(PRODUCT_NAME).dylib.dSYM" $(DEST_DIR)
    @set up "$(BUILD_DIR)/$(PRODUCT_NAME).swiftdoc" $(DEST_DIR)
    @set up "$(BUILD_DIR)/$(PRODUCT_NAME).swiftmodule" $(DEST_DIR)
    @set up "$(BUILD_DIR)/$(PRODUCT_NAME).swiftsourceinfo" $(DEST_DIR)
    @rm ./lib$(PRODUCT_NAME).dylib
    @rm -r ./lib$(PRODUCT_NAME).dylib.dSYM

uninstall: clear
    
    @rm $(DEST_DIR)lib$(PRODUCT_NAME).dylib
    @rm -r $(DEST_DIR)lib$(PRODUCT_NAME).dylib.dSYM
    @rm $(DEST_DIR)$(PRODUCT_NAME).swiftdoc
    @rm $(DEST_DIR)$(PRODUCT_NAME).swiftmodule
    @rm $(DEST_DIR)$(PRODUCT_NAME).swiftsourceinfo

clear:
    @swift bundle clear


Now for those who run make or make set up all of the required recordsdata shall be positioned beneath the proper location. Our dynamic library bundle is now prepared to make use of. The one query is how will we eat this shared binary library utilizing one other Swift Bundle goal? 🤔



Linking towards shared libraries

We will construct a model new executable software referred to as TextApp utilizing the Swift Bundle Supervisor. This bundle will use our beforehand created and put in shared dynamic library.



import PackageDescription

let bundle = Bundle(
    title: "TextApp",
    targets: [
        .target(name: "TextApp", swiftSettings: [
            .unsafeFlags(["-L", "/usr/local/lib/"]),
            .unsafeFlags(["-I", "/usr/local/lib/"]),
            .unsafeFlags(["-lTextUI"]),
        ], linkerSettings: [
            .unsafeFlags(["-L", "/usr/local/lib/"]),
            .unsafeFlags(["-I", "/usr/local/lib/"]),
            .unsafeFlags(["-lTextUI"]),
        ]),
    ]
)


The trick is that we will add some flags to the Swift compiler and the linker, so that they’ll know that we have ready some particular library and header (modulemap) recordsdata beneath the /usr/native/lib/ folder. We would additionally prefer to hyperlink the TextUI framework with our software, as a way to do that we now have to cross the title of the module as a flag. I’ve already defined these flags (-L, -I, -l) in my earlier posts so I suppose you are aware of them, if not please learn the linked articles. 🤓


import TextUI

print(TextUI.construct())

Our predominant.swift file is fairly simple, we simply print the results of the construct technique, the default implementation ought to return the well-known “Good day, World!” textual content.

Are you prepared to switch the construct operate utilizing native technique swizzling in Swift?



Dynamic technique substitute

After publishing my authentic plugin system associated article, I’ve received an e-mail from certainly one of my readers. To begin with thanks for letting me know concerning the @_dynamicReplacement attribute Corey. 🙏

The factor is that Swift helps dynamic technique swizzling out of the field, though it’s via a personal attribute (begins with an underscore), which suggests it’s not prepared for public use but (yeah… similar to @_exported, @_functionBuilder and the others), however finally it is going to be finalized.

You’ll be able to learn the unique dynamic technique substitute pitch on the Swift boards, there’s additionally this nice little snippet that accommodates a minimal showcase concerning the @_dynamicReplacement attribute.

Lengthy story quick, you should use this attribute to override a customized dynamic technique with your individual implementation (even when it comes from a dynamically loaded library). In our case we have already ready a dynamic construct technique, so if we attempt we will override that the next snippet.


import TextUI

extension TextUI {

    @_dynamicReplacement(for: construct())
    static func _customBuild() -> String {
        "It simply works."
    }
}

print(TextUI.construct()) 


For those who alter the predominant.swift file and run the undertaking it is best to see that even we’re calling the construct technique, it will be dispatched dynamically and our _customBuild() technique shall be referred to as beneath the hood, therefore the brand new return worth.

It really works like a allure, however can we make this much more dynamic? Is it doable to construct yet another dynamic library and cargo that at runtime, then substitute the unique construct implementation with the dynamically loaded lib code? The reply is sure, let me present you the way to do that. 🤩



import PackageDescription

let bundle = Bundle(
    title: "TextView",
    merchandise: [
        .library(name: "TextView", type: .dynamic, targets: ["TextView"]),
    ],
    targets: [
        .target(name: "TextView", swiftSettings: [
            .unsafeFlags(["-L", "/usr/local/lib/"]),
            .unsafeFlags(["-I", "/usr/local/lib/"]),
            .unsafeFlags(["-lTextUI"]),
        ], linkerSettings: [
            .unsafeFlags(["-L", "/usr/local/lib/"]),
            .unsafeFlags(["-I", "/usr/local/lib/"]),
            .unsafeFlags(["-lTextUI"]),
        ]),
    ]
)

Similar SPM sample, we have simply created a dynamic library and we have used the TextUI as a shared library so we will place our TextUI extension into this library as an alternative of the TextApp goal.

Thus far we have created 3 separated Swift packages shared the TextUI module between the TextApp and the TextView packages as a pre-built dynamic library (utilizing unsafe construct flags). Now we will lengthen the TextUI struct inside our TextView bundle and construct it as a dynamic library.


import TextUI

extension TextUI {

    @_dynamicReplacement(for: construct())
    static func _customBuild() -> String {
        "It simply works."
    }
}


We are able to use the same makefile (to the earlier one) or just run the swift construct -c launch command and duplicate the libTextView.dylib file from the construct listing by hand.

For those who run this code utilizing Linux or Home windows, the dynamic library file shall be referred to as libTextView.so beneath Linux and libTextView.dll on Home windows.

So simply place this file beneath your own home listing we will want the complete path to entry it utilizing the TextApp’s predominant file. We will use the dlopen name to load the dylib, it will substitute our construct technique, then we shut it utilizing dlclose (on the supported platforms, extra on this later…).


import Basis
import TextUI


print(TextUI.construct())


let dylibPath = "/Customers/tib/libTextView.dylib"
guard let dylibReference = dlopen(dylibPath, RTLD_LAZY) else {
    if let err = dlerror() {
        fatalError(String(format: "dlopen error - %s", err))
    }
    else {
        fatalError("unknown dlopen error")
    }
}
defer {
    dlclose(dylibReference)
}


print(TextUI.construct())


The wonderful thing about this strategy is that you do not have to fiddle with extra dlsym calls and unsafe C pointers. There’s additionally a pleasant and detailed article about Swift and native technique swizzling, this focuses a bit extra on the emitted replacements code, however I discovered it a really nice learn.

Sadly there’s yet another factor that we now have to speak about…



Drawbacks & conclusion

Dynamic technique substitute works good, this strategy is behind SwiftUI stay previews (or dlsym with some pointer magic, however who is aware of this for certain..). Anyway, every part appears to be like nice, till you begin involving Swift lessons beneath macOS. What’s unsuitable with lessons?

Seems that the Goal-C runtime will get concerned beneath macOS for those who compile a local Swift class. Simply compile the next instance supply and try it utilizing the nm instrument.



class A {}



Underneath macOS the output of nm will comprise traces of the Goal-C runtime and that’s greater than sufficient to trigger some troubles in the course of the dylib shut course of. Seems in case your library accommodates the ObjC runtime you will not be capable of really shut the dylib, it doesn’t matter what. ⚠️


Previous to Mac OS X 10.5, solely bundles could possibly be unloaded. Beginning in Mac OS X 10.5, dynamic libraries may be unloaded. There are a
couple of circumstances by which a dynamic library won’t ever be unloaded: 1) the primary executable hyperlinks towards it, 2) an API that doesn’t help
unloading (e.g. NSAddImage()) was used to load it or another dynamic library that is dependent upon it, 3) the dynamic library is in dyld’s
shared cache.


For those who check out man 3 dlclose you may get a couple of extra hints concerning the causes, plus you too can verify the supply code of the Goal-C runtime, if you wish to see extra particulars.


Anyway I believed this needs to be talked about, as a result of it could actually trigger some hassle (solely on macOS), however every part works simply nice beneath Linux, so if you’re planning to make use of this strategy on the server aspect, then I might say it’s going to work simply superb. It is not protected, nevertheless it ought to work. 😈


Oh, I virtually neglect the hot-reload performance. Nicely, you’ll be able to add a listing or file watcher that may monitor your supply codes and if one thing modifications you’ll be able to re-build the TextView dynamic library then load the dylib once more and name the construct technique if wanted. It is comparatively simple after you’ve got tackled the dylib half, as soon as you determine the smaller particulars, it really works like magic. 🥳





[ad_2]

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments