Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkgs/jni/lib/src/jclass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class JClass extends JObject {
JClass.forName(String name)
: super.fromReference(JGlobalReference(Jni.findClass(name)));

/// Constructs a [JClass] associated with the class or interface with
/// the given string name, using the global LRU cache.
JClass.forNameCached(String name)
: super.fromReference(JGlobalReference(Jni.getCachedClass(name)));

JConstructorId constructorId(String signature) {
return JConstructorId._(this, signature);
}
Expand Down
55 changes: 54 additions & 1 deletion pkgs/jni/lib/src/jni.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ abstract final class Jni {
static final DynamicLibrary _dylib = _loadDartJniLibrary(dir: _dylibDir);
static final JniBindings _bindings = JniBindings(_dylib);

static final _classCache = _JClassCache();

/// Store dylibDir if any was used.
static String _dylibDir = join('build', 'jni_libs');

Expand Down Expand Up @@ -194,6 +196,23 @@ abstract final class Jni {
.checkedClassRef;
}

/// Finds the class from its [name], using an internal LRU cache.
///
/// This is preferred for repeated lookups of the same class, as it avoids
/// repeated JNI calls and global reference creation.
static JClassPtr getCachedClass(String name) {
return _classCache.get(name);
}

/// Sets the capacity of the internal LRU class cache.
///
/// If the new size is smaller than the current number of cached classes,
/// the least recently used classes will be evicted and their global
/// references released.
static void setClassCacheSize(int size) {
_classCache.capacity = size;
}

/// Throws an exception.
// TODO(#561): Throw an actual `JThrowable`.
@internal
Expand Down Expand Up @@ -427,8 +446,42 @@ extension AdditionalEnvMethods on GlobalJniEnv {

@internal
extension StringMethodsForJni on String {
/// Returns a Utf-8 encoded `Pointer<Char>` with contents same as this string.
Pointer<Char> toNativeChars(Allocator allocator) {
return toNativeUtf8(allocator: allocator).cast<Char>();
}
}

class _JClassCache {
int _capacity = 256;
final _map = <String, JClassPtr>{};

JClassPtr get(String name) {
final existing = _map[name];
if (existing != null) {
// Move to end (MRU)
_map.remove(name);
_map[name] = existing;
return Jni.env.NewGlobalRef(existing);
}

final cls = Jni.findClass(name);

if (_map.length >= _capacity) {
final keyToRemove = _map.keys.first;
final valueToRemove = _map.remove(keyToRemove)!;
Jni.env.DeleteGlobalRef(valueToRemove);
}

_map[name] = cls;
return Jni.env.NewGlobalRef(cls);
}

set capacity(int size) {
_capacity = size;
while (_map.length > _capacity) {
final keyToRemove = _map.keys.first;
final valueToRemove = _map.remove(keyToRemove)!;
Jni.env.DeleteGlobalRef(valueToRemove);
}
}
}
29 changes: 29 additions & 0 deletions pkgs/jni/lib/src/third_party/jni_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,35 @@ class JniBindings {
late final _JniFindClass = _JniFindClassPtr.asFunction<
JniClassLookupResult Function(ffi.Pointer<ffi.Char>)>();

JniClassLookupResult GetCachedClass(
ffi.Pointer<ffi.Char> name,
) {
return _GetCachedClass(
name,
);
}

late final _GetCachedClassPtr = _lookup<
ffi.NativeFunction<
JniClassLookupResult Function(
ffi.Pointer<ffi.Char>)>>('GetCachedClass');
late final _GetCachedClass = _GetCachedClassPtr.asFunction<
JniClassLookupResult Function(ffi.Pointer<ffi.Char>)>();

void SetClassCacheSize(
int size,
) {
return _SetClassCacheSize(
size,
);
}

late final _SetClassCacheSizePtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int32)>>(
'SetClassCacheSize');
late final _SetClassCacheSize =
_SetClassCacheSizePtr.asFunction<void Function(int)>();

JniExceptionDetails GetExceptionDetails(
JThrowablePtr exception,
) {
Expand Down
115 changes: 115 additions & 0 deletions pkgs/jni/test/class_cache_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

@Tags(['load_test'])
library;

import 'dart:ffi';
import 'dart:io';

import 'package:jni/jni.dart';
import 'package:test/test.dart';

import 'test_util/test_util.dart';

void main() {
if (!Platform.isAndroid) {
spawnJvm();
}
run(testRunner: test);
}

void run({required TestRunnerCallback testRunner}) {
testRunner('Basic cache functionality', () {
final stringClass = Jni.getCachedClass('java/lang/String');
expect(stringClass, isNot(nullptr));

final stringClass2 = Jni.getCachedClass('java/lang/String');
expect(stringClass2, isNot(nullptr));
expect(stringClass.address, isNot(equals(stringClass2.address)));

// Check JNI equality (should be same object)
expect(Jni.env.IsSameObject(stringClass, stringClass2), isTrue);
Jni.env.DeleteGlobalRef(stringClass);
Jni.env.DeleteGlobalRef(stringClass2);
});

testRunner('Cache capacity configuration', () {
// Set small capacity
Jni.setClassCacheSize(2);

final c1 = Jni.getCachedClass('java/lang/String');
final c2 = Jni.getCachedClass('java/util/ArrayList');
// c3 would evict c1 if strictly LRU logic was only on insert,
// but lookup of c1 moves it to head? No, we haven't looked it up again yet.
// Insert c1. Head=c1. Size=1.
// Insert c2. Head=c2->c1. Size=2.
// Insert c3. Evict Tail(c1). Head=c3->c2. Size=2.
final c3 = Jni.getCachedClass('java/util/HashMap');

// Verify c1 still works (it's a new global ref)
expect(c1, isNot(nullptr));

// Verify we can reload c1
final c1Reload = Jni.getCachedClass('java/lang/String');
expect(Jni.env.IsSameObject(c1, c1Reload), isTrue);

Jni.env.DeleteGlobalRef(c1);
Jni.env.DeleteGlobalRef(c2);
Jni.env.DeleteGlobalRef(c3);
Jni.env.DeleteGlobalRef(c1Reload);
});

testRunner('Stress test cache', () {
Jni.setClassCacheSize(100);
// Load many classes
final classes = [
'java/lang/Byte',
'java/lang/Short',
'java/lang/Integer',
'java/lang/Long',
'java/lang/Float',
'java/lang/Double',
'java/lang/Boolean',
'java/lang/Character',
'java/lang/Object',
'java/lang/Class',
'java/lang/Number',
'java/util/List',
'java/util/Map',
'java/util/Set',
'java/util/Collection',
'java/util/Iterator',
'java/util/Random',
'java/io/File',
'java/io/InputStream',
'java/io/OutputStream',
];

for (var i = 0; i < 1000; i++) {
final name = classes[i % classes.length];
final cls = Jni.getCachedClass(name);
expect(cls, isNot(nullptr));
Jni.env.DeleteGlobalRef(cls);
}
});

testRunner('Cache hit returns new global ref', () {
final c1 = Jni.getCachedClass('java/lang/Object');
final c2 = Jni.getCachedClass('java/lang/Object');
// Pointers are different (different GlobalRefs)
expect(c1, isNot(equals(c2)));
// Objects are same
expect(Jni.env.IsSameObject(c1, c2), isTrue);

// Cleanup
Jni.env.DeleteGlobalRef(c1);
Jni.env.DeleteGlobalRef(c2);

// Cache should still hold a ref internally, so getting it again should work
final c3 = Jni.getCachedClass('java/lang/Object');
expect(c3, isNot(nullptr));
Jni.env.DeleteGlobalRef(c3);
});
}
2 changes: 1 addition & 1 deletion pkgs/jnigen/lib/src/bindings/dart_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ class _ClassGenerator extends Visitor<ClassDecl, void> {
final modifier = node.isTopLevel ? '' : ' static ';
final classRef = node.isTopLevel ? '_${node.finalName}Class' : '_class';
s.write('''
${modifier}final $classRef = $_jni.JClass.forName(r'$internalName');
${modifier}$_jni.JClass get $classRef => $_jni.JClass.forNameCached(r'$internalName');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've changed the code gen, so you'll need to regen all the bindings.


''');
return classRef;
Expand Down
Loading