Does anyone have working example of custom xcframe...
# multiplatform
a
Does anyone have working example of custom xcframework cinterop implementation? Context: Trying to achieve similar approach to firebase-kotlin-sdk, i.e. take custom iOS and Android libraries, add common code and publish it to maven for easy KMM consumption. Unfortunately copying their gradle configuration doesn't work for me, and tweaking it results in different kind of errors (e.g. linkDebugTestIos failures). Perhaps it's related to how xcframework is configured, I am not using Carthage or Cocoapods 🤷 Nonetheless would be great to see working examples of similar solutions if anyone has any.
👀 1
p
I tried to get it working for about 2 hours because I wanted to import stripe’s iOS framework in my kotlin code. Couldn’t get it to work, so I’d be very interested in a solution if you find one.
k
We're sort of doing this now. Firebase in particular can be difficult because it works with static frameworks (XC or "old style"), but with dynamic frameworks it's not linking properly. Still a work in progress there. https://github.com/touchlab/Kermit/tree/main/kermit-crashlytics
I don't actually use the full Firebase/Crashlytics dependency. Rather, I took the headers from Crashlytics and trimmed them down to only what we need (https://github.com/touchlab/Kermit/tree/main/kermit-crashlytics/src/include). That lets cinterop know what to generate and link against, and if you include Crashlytics in your app, they'll all work together happily. If you create a dynamic framework, it won't find Crashlytics and link against it. I'm not sure what
firebase-kotlin-sdk
does here, but should probably take a look. I'm trying to avoid being tied to cocoapods, but if you were just making an app as opposed to an sdk, you could let cocoapods handle it (yes, I know the stripe cocoapods integration wasn't working, but that's likely a cocoapods config thing)
We also have an unsupported plugin that will take c/objective-c (or c++ with
extern C
blocks), compile it, and insert that binary into your klib. So, if you had raw c/objc code that you wanted in your library, that might be an option (example: https://github.com/touchlab/Kermit/blob/main/kermit-crashlytics-test/build.gradle.kts#L69). It is unsupported because it's in flux and it only popped into existence because we needed it for a particular problem, but it does work.
I don't have a general answer to "how to include native code". There are a number of variables. However, you certainly can do it. A much simpler example is sqliter. The cinterop headers live there, but every system you can imagine has sqlite the native library available, so you just need to have the linker flag set: https://github.com/touchlab/SQLiter/tree/main/sqliter-driver
Was digging into this a bit but going through hell with cocoapods integration. It keeps telling me to install something that is installed. Need to shelve till later. Anyway, I think in our case, you could use cocoapods to pull in FirebaseCrashlytics along with our shared code that needs to link against that, and the next thing to try would be using SPM to include the same binary from Kotlin along with FirebaseCrashlytics. That would hopefully satisfy the link. I don't know if this would work with
linkDebugTestIos
. In earlier versions, you could not run command line build/tests if you were using cocoapods dependencies. You had to run from Xcode. SPM would also fail to run command line tests for the same reason. Kotlin builds an exe and doesn't know anything about your spm config, so wiring dependencies in that way would fail for sure.
You could add compiler and linker args to the test build only that point at binaries that satisfy those links. We're getting really into the weeds here, though, and it's extra hard if you're talking about publishing a library.
In the Kermit case, anyway, we have a stub implementation, which assumes you only need it to compile tests. For your app linking, you'd need to supply the "real" crashlytics. It's easier if your making static frameworks because you can satisfy those links at app assembly time in Xcode, but dynamic frameworks demand link satisfaction at Gradle build time (which, again, still sorting out, and I think Kotlin cocoapods integration would work, but I take back any speculation that SPM would. The Kotlin compiler is unaware of it. You may be able to use Kotlin Cocoapods to declare dependencies, then use SPM and satisfy them later, but I haven't tested that).
There's a lot I'm posting here, and it's clearly me thinking through it out loud to a large degree. Summary, you can do this, but there are a number of variables. I also regularly run into config rot. There aren't a ton of people doing this kind of thing, so running into issues that nobody has resolutions for is not uncommon, and things that should be smooth simply aren't. Again, currently stuck on
pod gen
bombing, but it works from the command line. My machine? Something with Kotlin 1.6.10? No idea, but need to shelve it for a bit and do "today" stuff.
a
Thanks for a thoughts dump Kevin, I really appreciate all the info. Definitely helps knowing I am not stupid and it is rather complicated topic 😄 After a lot of trial and error I managed to get my code to at very least compile. The custom framework I am trying to wrap isn't actually related to Firebase, I just used that firebase project as gradle configuration example, which after additional investigation I confirmed doesn't work on iOS target out of the box either. At this stage I have custom iOS framework & Android aar package that gets wrapped in a separate project with common Kotlin interface and then published to maven. "SharedA" module in a different KMM project then includes new library as a dependency. Though it will not compile until binaries are linked, and even once that is done if I want to use "ShareA" in let's say "ShareB" module as a dependency I need to link binaries in said module again. Might be easier to use cocoapods, but our custom framework is in cocoapods-art so that's another variable to figure out. Would be neat to somehow embed dynamic framework into a published library.
You mentioned unsupported plugin that would compile c/objective-c and include binary into klib. That's static right? Right now I am wondering if in a similar fashion I could take precompiled dynamic framework and either ship it inside or alongside klib to maven. At that point another plugin, perhaps providing custom configuration similar to
api
or
implementation
called
framework
does linking with a bundled binaries. Technically it wouldn't be that different from having to do manual linking or using cocoapods, plus it would require another plugin. However, from consumer perspective I see that as an easier route especially because you no longer need to be aware what frameworks third-party dependency wants you to link for iOS targets. That's ignoring the fact xcode project would still need to link the framework so I am just brainstorming.
k
The compiled c/objc is output in llvm bitcode, which is sort of similar to a static library (*.a) file. I'm sure there are technical differences that are important, but I don't know them off hand. In any case, that gets shipped with the klib and is statically pulled into the final binary. I don't know what the benefit of shipping a dynamic framework would be if you controlled and built the source yourself. If it's a 3rd party library, I guess my main concern would be that's it's a bespoke dependency management system, which would get painful quickly. If that dependency is 100% only used inside the Kotlin library code, it makes logical sense, but the final Xcode build would need to link it somehow (as you've mentioned).
I went down a rabbit hole yesterday after my posts. I remembered I'd been messing with my local config a bit a few days back and hadn't restarted my machine since. I did that and my cocoapods and Xcode started working again. Not sure why, but sometimes "reboot" is all you can do.
😄 1
Using Kermit's crashlytics with a dynamic framework actually works fine if you list crashlytics as a pod dependency
Copy code
cocoapods {
        //extra config was here...
        framework {
            isStatic = false
        }
        pod("FirebaseCrashlytics")
    }
That let's local command line testing work as well as building the framework, and although you still need to link it in Xcode, everything builds and works together. I think there was some kind of bug in earlier JB kotlin.cocoapods because that was not working as expected previously (tried it on kotlin 1.6.10)
a
Don't you have to do that for every module though? 🤔
k
I think that would also "work" with SPM. The Xcode project would simply wire in the kotlin framework and Crashlytics with SPM, as long as SPM included the right library
However, the cocoapods config does it's own cinterop, and we don't use that, so it's a waste. We only want to use cocoapods for dependency linking. I thought about this some time ago, but now am thinking about this again. Using cocoapods only to link and being able to disable the cinterop.
Don't you have to do that for every module though?
Not sure I understand what you mean. Every module that wants to include the Kermit-crashlytics?
Had a long answer but rethought it. Kermit is a special case which will make that config relatively easy. If you have a module that depends on an external dynamic framework for the majority of it's functionality, you would need some way to link it for testing (not really for compiling, though), then also a way to link it when building your final framework.
In that case, I do think you'd want to publish a custom gradle plugin, or some kind of KMP plugin extension, that manages custom compiler/linker options. We do some of this (none public right now, though 😞 ). Here's the extension function to add Intel and M1 simulators along with arm64 (it's like
ios()
)
Copy code
fun org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension.iosAll(
    namePrefix: String = "ios",
    configure: org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.() -> Unit = {}
) {
//...
🙏 1
a
Not sure I understand what you mean. Every module that wants to include the Kermit-crashlytics?
Multi-module KMM project. Say I have
base
module used by bunch of other KMM modules which includes Kermit-Crashlytics.
shared
module is the one being exposed to xcode and has dependency on
base
. Would I need cocoapods linking for Kermit on both
base
and
shared
modules or just one of them. Hope that makes my question clearer 😄
k
You'd have something like that that grabs the framework binary, then config all targets to add something link
-framework MySpecialFramework
to linker args for testing
Your question is clearer. Kermit is an easy edge case. The crashlytics module needs some special stuff, but in the normal course of usage, "base" wouldn't really need crashlytics. Just the core "Kermit" for the interface definitions (which the crashlytics module provides that talks to crashlytics). So, essentially, base wouldn't care. However, there's also a stub implementation specifically for testing (https://github.com/touchlab/Kermit/tree/main/kermit-crashlytics-test), which would let your tests compile and link, although it doesn't "do" anything. In this case, that would be desired, because you don't want to send crash reports from your local tests. So, Kermit is a special case. I think a library that actually needed to link to the real framework would need something like what I mentioned above. You can have a framework binary locally that gets linked by way of passing custom linker args, but you'll need a way to get that binary and put it where you need it.
I do think forking the JB kotlin.cocoapods plugin and being able to configure it just to pull in cocoapods dependencies for linking would be interesting. I've never really been a fan of how that works, as it's kind of an "all or nothing" setup. We used to have a fork because there was no way to configure the framework itself, but they've opened that up quite a bit, so if we can make a good case, we might be able to get that split up so you could just pull in and manage linking dependencies (but would need to play with it a lot more).
We are getting a lot more interest in SPM, so being able to support that as well as cocoapods will be increasingly important.
a
To be honest I find both cocoapods, carthage or SPM uses in KMM a bit odd right now. I have used them extensively in iOS projects for dependency management before, and I do see value it brings to KMM, I think of it as a bridge allowing us to use cocoapods dependencies that are otherwise not available through maven or even easily link shared code framework to iOS project in xcode. However gradle also offers dependency management so having to use both
dependencies {}
and then
cocoapods {}
for KMM dependencies doesn't feel great, reminds me of the times our project used cocoapods and carthage at the same time.
k
Well, I'd just ignore Carthage. This is a personal opinion, but we tried to integrate Carthage for an SDK client (they're an internal team publishing SDK's inside their org for other teams). They originally said Carthage was a requirement, but it was rough. Ran into all kinds of issues that ended with "submit a PR", often followed up with "we dropped Carthage support". That SDK project also needed Google sign in, and Google sign in has a new sdk that pretends Carthage doesn't exist. We got to the "end" of our first phase of that project and forced Carthage to work after quite a bit of hacking, and their first internal client said, "Oh, no, we're moving everything to SPM". So, personally I wouldn't consider Carthage support for anything as it seems like the community is giving up on it, but I'm not super tied into that community, so I could be wrong. Now, onto other thoughts...
Is maven very common for iOS framework dependencies? That seems very, very special case to me.
I don't think cocoapods or spm are a "bridge". I think it's kind of the other way around. For larger teams to consider KMM as a viable way to contribute code, it'll need to be relatively seamless in the iOS ecosystem. Otherwise KMM will stay pretty much for small teams and/or teams where Android is way more important to the product than iOS.
However gradle also offers dependency management
I guess you could have a different scope in gradle like
cocoapod("AlamoFire")
or
spm("whatever")
. That would look more like gradle, although I can't see how it's functionally much different than having the cocoapods block. It's more "gradle-formal", but you'd still need to wire things in on the iOS side. I haven't thought a ton about it, though.
a
Is maven very common for iOS framework dependencies? That seems very, very special case to me.
It's not common, haven't seen maven used for iOS dependencies ever. Just saying KMM dependencies come from maven though, so it shouldn't require other dependency management systems on top for any of the targets.
k
To some degree, we're driving way into an area that's mostly edge cases. I haven't had a ton of need for extensive cinterop with external Xcode frameworks. You do need it periodically, but in general you're kind of wrapping one big one for consumption in kotlin, so any value here is kind of mitigated by that (not sure what I mean there, just a thought. I don't see lots of ios deps alongside maven/kotlin ones).
a
Fair point. And I understand there are also complicated variables to consider. I think at this point I am basically just venting that it's not super duper user friendly which doesn't bring anything to the discussion 😄
k
so it shouldn't require other dependency management systems on top for any of the targets
Yeah, I guess that's what I'm saying. That's for sure not going to work, unless there's a wholesale import of ios frameworks into a maven repo somewhere. I would so , technically speaking, gradle dependencies can come from maven, but they can also come from ivy, local files, or custom repos (I believe). You can also write your own dependency management extensions, so rather than expect xcode frameworks somehow would wind up in a maven repo, the only way that would be feasible would be to write gradle dependency support for cocoapods repos (or spm, etc)
Oh, it is not user friendly. However, all I can say is I started poking around in 2017 and much more seriously in 2018. It has come quite a ways in that time 🙂 It should be easier still, but the native dependency in a kotlin library is less common, and I would imagine will be relatively unfriendly until somebody puts in some work there.
🙂 1