:wave: Hey everyone! I'm playing with a multi-modu...
# multiplatform
g
👋 Hey everyone! I'm playing with a multi-module KMM project and I've noticed the following behaviour: If one KMM module (
A
) declares a dependency on another KMM module (
B
), the generated Swift code will prefix the classes from
B
with its fully qualified module name when they are exposed through
A
. For example, a class
MyClass
defined in
B
but exposed through
A
will be available as
BMyClass
when the
A.framework
is used in Swift through
embedAndSignAppleFrameworkForXcode
. This is an issue when I have an interface which is defined in one module but implemented in another in which case Swift is not able to match the class names and does not consider the implementation as a valid argument when it is being passed as a parameter for example. Any ideas about this? Thanks in advance!
✅ 1
I've read in the roadmap to Beta that work is being done to better support multi-module projects which sounds related. At the moment, I have to call
embedAndSignAppleFrameworkForXcode
in all Xcode modules that I'd like to use KMM code which isn't ideal. 😞
I found a helpful SO post on the topic which led me to another great article here describing what's going on.
I don't know how to solve my issue off the top of my head but since I was looking for an explanation I'll consider this question answered and might post an update here for anyone else who comes across this 👍
t
If your goal is to have it non-prefixed when defining the framework you can use
export()
where you would do
export(projectB)
k
A few comments. WRT the blog post about multiple frameworks, you for sure need to wrap modules into a single framework and import that into Xcode. Being able to import multiple Kotlin Xcode frameworks that can communicate with each other is not likely to be possible any time soon. It’s a long story, but that’s by design.
t
Yeah that is a good point, If you have module A and module B you really should have a hierarchical order where the top level is the only one that is exported as a framework with the children modules bundled
k
You can export the module, as mentioned. However, I am going to submit a YT request to the team about finding better ways to control that naming. Exporting a module means anything public in it is added to the Objc header, even if you never need it in Swift, and anything exposed to Objc/Swift get an extra chunk of binary added for the interop. that all adds up and impacts the total binary size. We went deep on this working with clients who publish internal SDKs. A lot of these issues aren’t really a problem until you’re scaling, but they become issues.
t
the way that we have "worked around" this is by having internal submodules named "api" which are the only things exported. They have all "public api level" definitions that get exposed outside of the top level framework
Copy code
val iOSBinaryConfig: KotlinNativeTarget.() -> Unit = {
    binaries {
        framework {
            baseName = libraryName
            for (moduleDependency in moduleDependencies) {
                val apiProject = findProject("$moduleDependency:api")
                apiProject?.let { export(it) }
            }
            xcf.add(this)
        }
    }
}
moduleDependencies
is an internal variable in our scripts, so this isn't copy-pastable code
k
We’ve been experimenting with various module configs as well, although after a certain point it gets frustrating to try to leverage module structure to affect other things like visibility, which otherwise would have their own independent control. It gets extra hard when we want to expose coroutines stuff to Android but wrap that with callbacks on iOS, and not have the coroutines methods exposed directly. There are other issues too. It gets weird. Still experimenting.
The
api
thing is interesting, though. Will put that into the mix of things we’re trying.
t
Yeah for sure, def isn't a silver bullet and when onboarding people to the project it is a place that adds some confusion
the other neat thing it does allow us to do though is use the binary-comptaibility-validator on our api modules and our top level library
k
Well, the onboarding confusion list is long anyway, so… (jk)
true story 1
t
where the other modules don't need to be included with that check
k
Hmm. Interesting. We’re using that too, but right now just to keep an eye on the open source library drift.
t
I think the value/location of it differs on use-case. For us we have the KMP project isolated with it being consumed by the clients as a xcframework from swift package manager, gradle dep for android, and npm for web. Because it's much more of an "external library" the binary compatibility check just helps us prevent unintentional breaking api changes
Also in regards to the submodule
api
it did introduce some other complexities we had to work around in our build scripts (and if you misconfigure the binary validator it overwrites the folder named api) so another name might be helpful to use, and also be ready to work around some other things.
k
be ready to work around some other things
Always 🙂
g
Thanks both for the replies! 🙌 In my experience using something like an umbrella module which is exposed to Swift has the same naming effect - if module
A
exposes a class
Class
defined in module
B
, then the class will be visible to swift as
BClass
when linked using
./gradlew :A:embedAndSignAppleFrameworkForXcode
, unless I'm missing something... I didn't know about the
export(projectB)
option though so will read up on it and give it a try 👍
The
export(...)
trick worked by the way, thanks so much again!
🦜 1
s
Just came across this post and the
export()
solved my issue! Thanks!
🙌 1
a
Amazing, had the sam issue. However
export()
does exactly what I need. Are there any potential limitations using this approach? Also noticed that
transitiveExport
is not needed in this case?
k
I wouldn’t say “limitations”, but
export()
can be a problem if you don’t want to export the entire dependency. So, say you were using a library and just really needed 10% of it to be addressable in Swift, and maybe only use 50% of it total. 100% of the library is retained when exported (well, the public parts anyway). Anything you don’t need winds up cluttering the header, but also each class that Swift needs to talk to gets some extra binary from the compiler to manage the objc interface. That can increase binary size, although “how much?” is hard to say off hand.
transitiveExport
has the same problems, but can be significantly worse if you have a lot of library code that either isn’t used at all or doesn’t need to be publicly visible to Swift. Some discussion on binary size and visibility:

https://youtu.be/hrRqX7NYg3Q?t=1893▾

🙌 2
a
@kpgalligan Thank you so much for additional explanation and video!