Methods to load a dynamic library and use native methodology 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, you need to positively 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 package deal = Bundle(
title: "TextUI",
merchandise: [
.library(name: "TextUI", type: .dynamic, targets: ["TextUI"]),
],
dependencies: [
],
targets: [
.target(name: "TextUI", swiftSettings: [
.unsafeFlags(["-emit-module", "-emit-library"])
]),
]
)
The package deal manifest is kind of easy, though there are a number 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 make sure that the best .dylib (or .so / .dll) binary might be created while you construct the goal. 🎯
The second factor is that we might prefer to emit our Swift module data 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 might be immediately 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 methodology) you inform the compiler that it ought to use dynamic dispatch to “resolve” the implementation when calling it.
We’ll make the most of the dynamic dispatch afterward, however earlier than we may transfer onto that half, we’ve to construct our dynamic library and make it obtainable for others to make use of. 🔨
In case you run swift construct (or run the mission by way of Xcode) it’s going to construct all of the required recordsdata and place them below the correct construct folder. You too can print the construct folder by operating 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). In case you record the contents of the output listing, you need 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’ll want them below a location the place the construct instruments can entry the associated recordsdata, for the sake of simplicity we will put every thing 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 package deal clear
Now when you run make
or make set up
all of the required recordsdata might be positioned below the best location. Our dynamic library package deal is now prepared to make use of. The one query is how can we eat this shared binary library utilizing one other Swift Bundle goal? 🤔
Linking in opposition to shared libraries
We’ll construct a model new executable software known as TextApp utilizing the Swift Bundle Supervisor. This package deal will use our beforehand created and put in shared dynamic library.
import PackageDescription
let package deal = 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 below the /usr/native/lib/
folder. We might additionally prefer to hyperlink the TextUI
framework with our software, in an effort to do that we’ve to move 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 important.swift
file is fairly easy, we simply print the results of the construct methodology, the default implementation ought to return the well-known “Good day, World!” textual content.
Are you prepared to interchange the construct operate utilizing native methodology swizzling in Swift?
Dynamic methodology substitute
After publishing my authentic plugin system associated article, I’ve bought an electronic 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 methodology swizzling out of the field, though it’s via a personal attribute (begins with an underscore), which suggests it isn’t prepared for public use but (yeah… similar to @_exported
, @_functionBuilder
and the others), however finally it will likely be finalized.
You may learn the unique dynamic methodology substitute pitch on the Swift boards, there’s additionally this nice little snippet that accommodates a minimal showcase concerning the @_dynamicReplacement
attribute.
Lengthy story brief, you should utilize this attribute to override a customized dynamic methodology with your personal implementation (even when it comes from a dynamically loaded library). In our case we have already ready a dynamic construct methodology, so if we strive we will override that the next snippet.
import TextUI
extension TextUI {
@_dynamicReplacement(for: construct())
static func _customBuild() -> String {
"It simply works."
}
}
print(TextUI.construct())
In case you alter the important.swift
file and run the mission you need to see that even we’re calling the construct methodology, it’ll be dispatched dynamically and our _customBuild()
methodology might be known as below 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 another dynamic library and cargo that at runtime, then exchange the unique construct implementation with the dynamically loaded lib code? The reply is sure, let me present you ways to do that. 🤩
import PackageDescription
let package deal = 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 a substitute of the TextApp goal.
To this point 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 prolong the TextUI struct inside our TextView package deal 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.
In case you run this code utilizing Linux or Home windows, the dynamic library file might be known as libTextView.so
below Linux and libTextView.dll
on Home windows.
So simply place this file below your house listing we will want the total path to entry it utilizing the TextApp’s important file. We’ll use the dlopen
name to load the dylib, it will exchange our construct methodology, 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 beauty of this strategy is that you do not have to fiddle with extra dlsym
calls and unsafe C pointers. There may be additionally a pleasant and detailed article about Swift and native methodology swizzling, this focuses a bit extra on the emitted replacements code, however I discovered it a really nice learn.
Sadly there may be another factor that we’ve to speak about…
Drawbacks & conclusion
Dynamic methodology substitute works good, this strategy is behind SwiftUI reside previews (or dlsym with some pointer magic, however who is aware of this for positive..). Anyway, every thing seems nice, till you begin involving Swift courses below macOS. What’s improper with courses?
Seems that the Goal-C runtime will get concerned below macOS when you compile a local Swift class. Simply compile the next instance supply and try it utilizing the nm software.
class A {}
Beneath macOS the output of nm will include 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 to truly 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 additionally be unloaded. There are a
couple of circumstances during which a dynamic library won’t ever be unloaded: 1) the principle executable hyperlinks in opposition to it, 2) an API that doesn’t assist
unloading (e.g. NSAddImage()) was used to load it or another dynamic library that relies on it, 3) the dynamic library is in dyld’s
shared cache.
In case you check out man 3 dlclose
you may get a number 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 assumed this needs to be talked about, as a result of it will probably trigger some bother (solely on macOS), however every thing works simply nice below Linux, so in case you are planning to make use of this strategy on the server facet, then I might say it’s going to work simply fantastic. It isn’t secure, but it surely ought to work. 😈
Oh, I nearly overlook the hot-reload performance. Effectively, you possibly can add a listing or file watcher that may monitor your supply codes and if one thing modifications you possibly can re-build the TextView dynamic library then load the dylib once more and name the construct methodology if wanted. It is comparatively straightforward after you have tackled the dylib half, as soon as you determine the smaller particulars, it really works like magic. 🥳