Hello everyone. I work on a Kotlin library that us...
# server
t
Hello everyone. I work on a Kotlin library that uses the Ubyte, UInt, and ULong types. A few Java devs have told us they cannot use our constructors because of these types which are not understood by Java, and so far the only solution I have seen to fix this is to write small Kotlin wrappers around our data classes which use unsigned types. I’m wondering if anyone here has other, Java-only solutions for how to work with these unsigned types. This would allow us to provide solutions that don’t rely on adding Kotlin to our Java users’ codebases. Thanks!
c
UByte
etc are inline classes, they should not be visible from Java. For example,
fun foo(a: UInt) = a + 1u
has the Java signature
Copy code
int foo(int a)
If they cannot call them directly like this, it's likely you have some other problem, can you share an example?
t
Sure! This is an example type:
Copy code
data class BlockTime (
    var height: UInt, 
    var timestamp: ULong
)
An attempt to create an object of this type will give you this error:
Copy code
BlockTime blockTime = new BlockTime(
    10, 
    10000000
);
// 'BlockTime(int, long)' has private access in org.example.BlockTime
We use uniffi-rs to produce language bindings from Rust to Kotlin, so it’s likely that there are more intricacies there, but the Kotlin data class definition is really just what I have above.
k
Well, at the first glance, it looks completely unrelated to unsigned types and more like private constructor of BlockTime 🤔 Have you tried to add
public
modifier?
t
Yeah these are totally public classes!
k
Do you have some sample to reproduce?
t
Yep let me find you something.
This repo can be used to test out some of the types that don’t seem to port over:
k
I'll look slightly later, ok?
t
Oh no rush at all! We’re just trying to figure out exactly where the issue is and where to track it (whether it’s on our side or on Mozilla’s library side). Take you time. It’s something I’ve been meaning to deal with for a while but not an urgent matter. We have workaround solutions that do work at the moment.
🤝 1
c
There is no Kotlin in your example. I suspect the issue is with the other plugins you're using, could you try adding another module to your reproduction with your value classes and check if they work together?
t
Not sure I get your meaning sorry. Happy to fix/add anything. The issue in my working example comes from attempting to build an object of type BlockTime (which is defined in our bdk-jvm library). Command-clicking into this type will show you the following:
Copy code
public final data class BlockTime public constructor(height: kotlin.UInt, timestamp: kotlin.ULong) {
    public final var height: kotlin.UInt /* compiled code */

    public final var timestamp: kotlin.ULong /* compiled code */

    public final operator fun component1(): kotlin.UInt { /* compiled code */ }

    public final operator fun component2(): kotlin.ULong { /* compiled code */ }
}
And the source file is this:
Copy code
data class BlockTime (
    var `height`: UInt, 
    var `timestamp`: ULong
)
c
Sorry, I don't have the time to test it myself. Can you add the
kotlin("jvm")
plugin to your repo, and add a copy of your
BlockTime
class in
src/main/kotlin/…
and try to use it? Your reproduction repo doesn't have any weird plugins, so if it works for this you'll be able to search for the differences with your libraries.
t
Yeah good idea. I’m actually able to reproduce the issue indeed. Adding a new Kotlin data class to the project:
Copy code
data class BlockTime2 (
    var height: UInt,
    var timestamp: ULong
)
Results in the same error:
Copy code
/Users/user/repos/bdk-java-interop/src/main/java/org/sandbox/Main.java:33: error: BlockTime2(int,long) has private access in BlockTime2
        BlockTime2 blockTime = new BlockTime2(
                               ^
When attempting to create the object from the Java side
These errors go away if I redefine my data class to use Int and Long instead of UInt and ULong. (I added the new code to the repo if you’d like to see the issue in action)
k
I pasted your BlockTime2 class into IDEA, did Tools -> Kotlin -> Show Kotlin Bytecode, and then decompiled into Java and got this:
Copy code
private BlockTime2(int height, long timestamp) {
      this.height = height;
      this.timestamp = timestamp;
   }

   // $FF: synthetic method
   public BlockTime2(int height, long timestamp, DefaultConstructorMarker $constructor_marker) {
      this(height, timestamp);
   }
So yes, indeed, it doesn't work from Java. You may be able to hack using some dummy DefaultConstructorMarker parameter.
k
Yes, this is the problem, but the most interesting question - what's root cause? If you replace unsigned types with signed, constructor becomes public :)
c
That's very weird, and completely different from my understanding from reading the docs. Now that you have a reproduction, I believe it's worth filing as a ticket (https://kotl.in/issue) to get an official answer on what's going on here (if you do, please link it here so we can subscribe to it).
t
Sounds good. Thanks for the help guys!
g
Hi, UInt, and ULong are represented as value classes in stdlib (as it is mentioned already above). Therefore, calling the code from java you should disable the mangling like this:
OtherBlockTime.kt
:
Copy code
@file:JvmName("BlockTimeKt")

package org.sandbox

data class BlockTime2(
    var height: UInt,
    var timestamp: ULong
)

@Suppress("FunctionName")
@JvmName("from") // disables mangling
fun BlockTime2(height: Int, timestamp: Long): BlockTime2 {
    return BlockTime2(height.toUInt(), timestamp.toULong())
}

data class BlockTime3(
    var height: Int,
    var timestamp: Long
)
`Main.java`:
Copy code
public class Main {
    public static void main(String[] args) throws BdkException {

        System.out.println("Hello world!");
        blockTime();
    }

    public static void blockTime() {
//        BlockTime blockTime = new org.bitcoindevkit.BlockTime(
//            10,
//            10000000
//        );

        BlockTime2 blockTime = BlockTimeKt.from(
            10,
            10000000
        ); // works

        BlockTime3 blockTime3 = new BlockTime3(
            10,
            10000000
        );
    }
}
See here for more about mangling. That being said, imho (depending on your use case ofc) if you really like to represent in your lib Unsigned types and to be used by java code i would go ahead and create my own types as simple data classes. Or (just thought of this) create new wrapper types only for java code and play around them with factory functions (i like a tad more this approach).
k
I’m not sure that it should have any deal with mangling at all. We can just add secondary constructor and it should work (I hope):
Copy code
data class BlockTime(
    var height: UInt,
    var timestamp: ULong
) {
    constructor(
        height: Int,
        timestamp: Long
    ) : this(
        height.toUInt(),
        timestamp.toULong()
    )
}
Problem is - this code is autogenerated from Rust and it requires changes in generator from library’s authors
g
I think you can disable mangling only with "factory" functions (similar to the example in kotlin docs), since
@JvmName
does not work on constructors.
k
Constructors are not mangled anyway, afaik 🙂
c
Or (just thought of this) create new wrapper types only for java code and play around them with factory functions
IMO, if factory functions are needed for Java anyway, it's easier to just declare them with
@JvmName
than to create a whole new class
🤔 1
k
Well, my code above won’t work because of declaration clash. And that’s funny because idea doesn’t show any warning/error at all
g
Updating my answer:
@JvmName
does not disables mangling in my example, it is just for a nicer java API. The "bridge" function is still needed though.
t
Awesome thanks.
👍 1
Yeah the bridge function is basically the problem I want to get around. For example our library uses language bindings produced by uniffi-rs, and so I don’t have the ability to add these bridge functions directly. This means users must add the Kotlin toolchain to their projects and create the wrappers themselves if they wish to use our stuff on the Java side.
k
@thunderbiscuit try to write library’s authors to improve their codegen
1
t
One sort-of solution proposed by a user is reflection. Take a look at this:
Copy code
Constructor<ElectrumConfig> electrumConfigConstructor = (Constructor<ElectrumConfig>) ElectrumConfig.class.getDeclaredConstructors()[1];
electrumConfigConstructor.setAccessible(true);
ElectrumConfig electrumConfig = electrumConfigConstructor.newInstance(electrumAddress, null, Byte.parseByte("5"), null, 5, true);
👀 1
This does work, although… it’s hacky.
k
That’s always slower and insecure - it’ll break in runtime if constructor changes
👍 2
106 Views