-
Notifications
You must be signed in to change notification settings - Fork 234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix JNA bug MethodTooLargeException
by splitting big UniffiLib
interface
#2344
Fix JNA bug MethodTooLargeException
by splitting big UniffiLib
interface
#2344
Conversation
// so that we can call `uniffiCheckApiChecksums`... | ||
loadIndirect<UniffiLibChecksums>(componentName) | ||
.also { lib: UniffiLibChecksums -> | ||
uniffiCheckApiChecksums(lib) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me know if I should adjust interface UniffiLibChecksums
to contain the ffi_uniffi_contract_version
function so that we can call uniffiCheckContractApiVersion(lib)
inside this .also
block, which would be closer to what we do on main
branch (call uniffiCheckContractApiVersion
before uniffiCheckApiChecksums
).
Alternatively I can change to this to ensure uniffiCheckContractApiVersion
is called before uniffiCheckApiChecksums
:
internal val INSTANCE: UniffiLib by lazy {
val lib = loadIndirect<UniffiLib>(componentName)
.also { lib: UniffiLib ->
uniffiCheckContractApiVersion(lib)
}
// check checksums
loadIndirect<UniffiLibChecksums>(componentName)
.also { lib: UniffiLibChecksums ->
uniffiCheckApiChecksums(lib)
}
lib // return lib
}
Let me know what you prefer:
- keep as is
- move
ffi_uniffi_contract_version
intoUniffiLibChecksums
(and maybe rename the interfaceUniffiLibChecks
) - change
internal val INSTANCE: UniffiLib
so that we loadlib: UniffiLib
lib first then do checksum checks and returnlib
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer 2
, since a contract version change could potentially affect the checksum function signature. However, if this makes the interface
code more complicated, it's not worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will give it (option 2) a go and will ping you tomorrow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bendk OK I've renamed to new lib IntegrityCheckingUniffiLib
(instead of UniffiLibChecksums
) since it now also performs the contract version check. I think "integrity check" is OK terminology to describe the aggregation of "contract version check" and "checksum checks".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bendk I've ensured to not get lint errors (which I got at first) if lib: UniffiLib
is not used - which can be the case now if initialization_fns()
is empty.
I've updated to only conditionally include the .also
after loading lib, only if initialization_fns()
is not empty.
I think that is the best solution. Let me know if you are happy with it.
uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt
Show resolved
Hide resolved
uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt
Show resolved
Hide resolved
uniffi_bindgen/src/bindings/kotlin/templates/NamespaceLibraryTemplate.kt
Outdated
Show resolved
Hide resolved
We run some tests with @Sajjon measuring the time difference it takes to resolve the We didn't notice any relative performance hit. We built sargon-uniffi crate in debug mode and exported two android The resulting android app was also built in debug mode and run on android emulator and in a relatively low spec Samsung device (Samsung A53). Keep in mind that these measurements are always inflated in debug mode. Emulator: ~1ms of max difference (sometimes the instance was resolved quicker with the fix). (65.8ms ~ 66.8ms) Although we were not able to minimise the noise affected by other factors (especially with the real device), we didn't observe any noticeable performance hit. |
@bendk let me know if you need something else to get this merged! :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this. Hopefully this is enough to avoid the issue although I guess if your library was 2x as big then you'd run into it again. I'm flagging this as requesting changes, just because there was the question of how to handle the library version check. If you think the current way is the cleanest, just say so and I'm happy to take it as-is.
I'm really wondering if all these checksum checks are worth the complexity, but I don't think we're going to make a decision on that soon. In the meantime, this seems like a good workaround.
// so that we can call `uniffiCheckApiChecksums`... | ||
loadIndirect<UniffiLibChecksums>(componentName) | ||
.also { lib: UniffiLibChecksums -> | ||
uniffiCheckApiChecksums(lib) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer 2
, since a contract version change could potentially affect the checksum function signature. However, if this makes the interface
code more complicated, it's not worth it.
…hecksum fn from UniffiLib interface into separate one.
c65ed07
to
20fe81c
Compare
…empty (conditionally add `.also` after loading library).
Yes if we were to 2x our lib we would be hit by this again, that is true. But I expect us to grow our API with 30-50% only (i.e. less than 100%) in the coming months, and then actually decrease the UniFFI exported API "surface" as we are able to use more complex flows in Rust, thus reducing need for some methods and records :) For the futureTip If future readers are hit by We can note that it is possible to accommodate even bigger libraries if we were to split both fun gradient(ln: Line): kotlin.Double {
FfiConverterDouble.lift(
uniffiRustCall { _status ->
UniffiLibBatch1.INSTANCE.uniffi_uniffi_geometry_fn_func_gradient(
FfiConverterTypeLine.lower(ln), _status
)
}
)
} Should it be |
@skeet70 do you have any thoughts here? uniffi-bindgen-java will have the same problem I assume? |
Ah that's unfortunate, along with the fix introducing more initial delay and if needed again doesn't scale well. I don't have a better solution though. You're right @mhammond that the Java bindings are likely affected by the same bug, I'll make a ticket. All classes/interfaces are split out into individual files there, but if the problem is a single interface getting too large, it'll still be an issue. |
I think #2333 might help with this if we ever needed it (especially after I make some changes suggested by @mhammond). The general idea is to add a transformation pass where we convert the generalized interface into the Kotlin-specific interface. Part of that could be to take the FFI functions and split them up into different batches and also associate the batch number with each FFI function reference. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look great to me, I'd be good with merging. Are there any remaining concerns?
Good to merge from my perspective, what say @mhammond ? |
This PR solves #2340 Kotlin bug which is caused by a limit in JNA causing Kotlin generated code to throw
MethodTooLargeException
at runtime.Note
The name
MethodTooLargeException
is a red herring! This is super confusing since it is, in fact, not at allany method being too large, it is an interface being too large, specifically the
UniffiLib
.Tip
Apart from all unit tests passing locally:
I've asserted that this change works. See failing Kotlin (JVM) tests in Sargon CI before this fix (many
jdk.internal.org.objectweb.asm.MethodTooLargeException
all over the place) and see successful CI when I use my UniFFI fork with this fixWe at Radix got hit with this bug because apparently
Sargon
is the worlds biggest UniFFI crate with Kotlin binding :). The reason why Mozilla (or any other company) has not been hit with this JNA bug is probably because you use a multi-crate setup -Sargon
does not (yet).This PR pushes the limit of methods/functions in a UniFFI Kotlin project by a factor of ~2x. The solution is actually quite simple:
uniffi_***_uniffi_checksum_func_***
functions and the check contract version function out frominterface UniffiLib
and put them into a newinterface IntegrityCheckingUniffiLib
UniffiLib
's singletonINSTANCE
is loaded the checksums is just called once. And not referenced from anywhere else.internal val INSTANCE: UniffiLib
lazy init, to load the library withloadIndirect
first asIntegrityCheckingUniffiLib
and then calluniffiCheckContractApiVersion
and thenuniffiCheckApiChecksums
and then load the library again withloadIndirect
asUniffiLib
.loadIndirect
just once, and casting it, but that does not work since JNA load creates a proxy instance which cannot be cast later on, however...loadIndirect
is~50ms
on JVM macOS Sonoma | Mac Studio M1 Max (slower on slow Android devices ofc). Since this is a one time cost (well two times now) and typically still fast, this seems acceptable.Changes expressed in generated Kotlin Code