2016. dec 26.

`internal` láthatóság Kotlinban

írta: vilmos.nagy

Egy ideje játszadozom a Kotlin programozási nyelvvel (aminek az 1.0-s kiadása lassan ünnepli az egyéves kort), s az utóbbi időben elkezdtem gondolkozni azon, vajon hogyan valósították meg az internal modifiert a JVM-en.

Olyan szempontból érdekelt a kérdés, hogy a Java nyelvben jelenleg nem szerepel az internal modifier (legjobb tudomásom szerint nincs is hasonló tervben - igen, Jigsaw, lásd később), s én nem is tudnám hirtelen leimplementálni a JVM-re. Ami engem érdekelt, hogy a JetBrainsnél sikerült-e ezt beleerőszakolni a JVM-be, s ha igen, hogyan. 

Hogy mit is csinál ez a láthatóság? Idézem a Kotlin dokumentáció megfelelő sorait:

The internal visibility modifier means that the member is visible with the same module. More specifically, a module is a set of Kotlin files compiled together:

  • an IntelliJ IDEA module;
  • a Maven or Gradle project;
  • a set of files compiled with one invocation of the Ant task.

Ehhez képest a Java dokumentációja nem tesz még csak modulról sem említést, nemhogy ehhez hasonló láthatóságról:

Access Levels

Modifier Class Package Subclass World
public Y Y Y Y
protected Y Y Y N
no modifier Y Y N N
private Y N N N

 

Sőt, jelenleg a Javaban még csak a modul fogalma sem teljesen definiált. Beszélünk Maven modulokról, Gradle projektekről, de ezek végeredményben csak egy adag, lefordított Java class fájlt eredményeznek - amit esetleg becsomagolunk egy jar, war vagy ear fájlba. Itt egy kicsit csúsztatok, mert a Java dokumentációja, de még maga a Java nyelv sem tekinthető a JVM specifikációjának - de ezt most nézzük el nekem, s higgyük el, hogy tényleg nincsenek ilyenek még a JVM-ben. :)

A Java 9, s a Project Jigsaw meghozza majd a Java világába is a modul fogalmát, s bizonyos értelemben a modulon kívülre, s a csak belső használatra szánt osztályok/metódusok megkülönböztetését. De:

  • A Java 9 még nincs itt,
  • a Kotlin Java 6-tal kompatibilis bátjkódot fordít, 
  • s a Jigsaw-ban kicsit kevésbé finoman lehet majd megszabni egy osztály/metódus láthatóságát.

A félreértések elkerülése végett, posztban én a Kotlin által definiált modul fogalmat használom. A legpontosabb, s legtöbb, amit a témában találtam, az ez JetBrains blogposzt. Röviden összefoglalva, két dolgot állít a fenti blogposzt:

  • az internal láthatóságú függvények a Java public láthatóságának megfelelően kerülnek bele a lefordított végeredménybe,
  • és hogy a metódus/osztály neve megváltozik (name mangling).
    • ennek egy apró mellékhatása, hogy Java-ból, egy internal láthatóságú Kotlin metódust sem tudunk meghívni, még ha egy modulban vannak, akkor se.

Az fenti két megállapítás közül az első eléggé egyértelmű: a Kotlin specifikációja szerint az internal láthatóságú metódus/osztályt tágabb körben látja bármi, mint ami a JVM-en a második legkorlátozóbb láthatóságnál (protected) adott. 

A második viszont érdekes kérdéseket vet fel:

  • Miért van szükség arra, hogy megváltoztassuk a lefordított metódus/osztály nevét?
  • S ha kitalálom valahogyan a nevét az osztálynak/metódusnak, akkor tudom-e egy más modulban lévő Java/Kotlin kódból hívni.

Az elsőre egyszerű válasz lehetne, hogy azért, változtatjuk meg a metódus nevét, mert nem akarom, hogy azok, akik használják az én modulomat, megtalálják egyszerű név alapján a metódust :). De nem pontosan ez a válasz, s ezt a kérdést a linkelt JetBrainses blogban részletesen tárgyalják.

Mikor elkezdtem vizsgálni, hogy mégiscsak tudom-e használni egy másik Java/Kotlin modulból az internal produktomomat, akkor érdekes eredményre jutottam: Kotlinból már példányosítani sem tudom az internal osztályomat, se meghívni egy internal metódust. Ez várható volt, hiszen a Kotlin fordító fel van készítve arra, hogy Kotlinból fordított osztályokkal dolgozzon - s el tud helyezni magának meta-információkat a fordított class fájlba, amit utána értelmezhez.

Azonban Javaból gond nélkül tudok példányosítani egy másik modulbeli osztályt - s egy kis trükkel, még egy internal metódusra is rá tudok hívni.

A kipróbálásra használt forráskód faék egyszerű:

Kicsit vágtam a képen, de nagyjából átlátható, remélem. Adott két modul: X és Y. Y modul Foo osztálya internal láthatósággal, s az X modulból ezt próbálom meg kétféleképpen (Kotlin/Java) is használni. Töltsd le Githubról, s nézd végig, ott részletesebben kommentáltam a kóddal kapcsolatos dolgokat. 

Na, de nézzünk a motorháztető alá! A Foo osztály valahogy így néz ki:

internal class Foo {
init {
println("Foo constructor")
}

internal fun foo() {
println("Foo::foo method")
}
}

Ebből a következő Foo.class fájl fordul:

$ javap -c -p Foo.class
Compiled from "Foo.kt"
public final class Foo {
public final void foo$y_main();
Code:
0: ldc #8 // String Foo::foo method
2: astore_1
3: nop
4: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_1
8: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
11: return

public Foo();
Code:
0: aload_0
1: invokespecial #25 // Method java/lang/Object."<init>":()V
4: ldc #27 // String Foo constructor
6: astore_1
7: nop
8: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: return
}

Azt már a fájlnévből látjuk, hogy az osztály neve nem módosul - ez egyébként egy érdekes kérdés, miért nem oszt neki random nevet a Kotlin compiler, s én nem is tudnám megindokolni. De a metódus neve változik.

Ha egy, az Y modulon kívüli (de akár belüli) Java osztályban próbálom meghívni ezt a metódust, akkor az osztály lefordul, lefut.

Ami még érdekes lehet, hogy verbose módban a javap azt is megmutatja, hogy a Foo osztályra kerül egy kotlin.Metadata annotáció. Egy kicsit okosabb decompilerrel vissza lehet akár ezt is fordítani (vagy kézzel a constant pool table-ből kikeresve az értékeket), s tovább vizsgálni, mi is történhet a háttérben. 

Összefoglalva: az internal láthatóság egy nagyon jó iránynak indul, valós problémákat oldana fel (khm, sun.misc.Unsafe, khm), s végre elfelejti a Javaból örökölt package alapú megoldást - aminek sok haszna nincs. Reményeim szerint a Java 9, s a Project Jigsaw után még csavarnak egyet ezen, s kapunk egy ténylegesen internal láthatóságot. Nem merném azt mondani, hogy jelenleg ez felesleges. De szerintem nem árt tisztában lenni azzal, hogy a JVM-en a Kotlin internal láthatósága nem jelent teljes védelmet.

Szólj hozzá

jvm kotlin internal modifier bájtkód