Skip to content

Commit 7bd2104

Browse files
authored
leveldb native interop example (#35)
2 parents 2e59a45 + 4bd7f4c commit 7bd2104

File tree

8 files changed

+341
-0
lines changed

8 files changed

+341
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/mvn_java
2+
/.dart_tool
3+
.DS_Store
4+
/.idea
5+
/build

jnigen_db_native_interop/LICENSE

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2025, James Williams
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
8+
1. Redistributions of source code must retain the above copyright notice, this
9+
list of conditions and the following disclaimer.
10+
11+
2. Redistributions in binary form must reproduce the above copyright notice,
12+
this list of conditions and the following disclaimer in the documentation
13+
and/or other materials provided with the distribution.
14+
15+
3. Neither the name of the copyright holder nor the names of its
16+
contributors may be used to endorse or promote products derived from
17+
this software without specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

jnigen_db_native_interop/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# jni_leveldb
2+
3+
A sample command-line application demonstrating how to create an idiomatic Dart wrapper around a Java implementation of LevelDB, a fast key-value storage library. This project showcases the evolution of a LevelDB wrapper from raw JNI bindings to a safe, clean, and Dart-native API using `jnigen`.
4+
5+
## Prerequisites
6+
7+
- Java Development Kit (JDK) version 1.8 or later must be installed, and the `JAVA_HOME` environment variable must be set to its location.
8+
9+
## Setup and Running
10+
11+
1. **Generate Bindings and Build JNI code:**
12+
13+
Run `jnigen` to download the required Java libraries, generate Dart bindings, and build the JNI glue code.
14+
15+
```bash
16+
dart run jni:setup
17+
dart run jnigen:setup
18+
dart run jnigen --config jnigen.yaml
19+
```
20+
21+
2. **Run the application:**
22+
23+
```bash
24+
dart run bin/jni_leveldb.dart
25+
```
26+
27+
## From Raw Bindings to an Idiomatic Dart API
28+
29+
The initial output of `jnigen` provides a low-level, direct mapping of the Java API. Using this directly in application code can be verbose, unsafe, and unidiomatic. This project demonstrates how to build a better wrapper by addressing common pitfalls.
30+
31+
### 1. Resource Management
32+
33+
JNI objects are handles to resources in the JVM. Failing to release them causes memory leaks.
34+
35+
**The Problem:** Forgetting to call `release()` on JNI objects.
36+
37+
**The Solution:** The best practice is to use the `using(Arena arena)` block, which automatically manages releasing all objects allocated within it, making your code safer and cleaner. For objects that live longer, you must manually call `release()`.
38+
39+
*Example from the wrapper:*
40+
```dart
41+
void putBytes(Uint8List key, Uint8List value) {
42+
using((arena) {
43+
final jKey = JByteArray.from(key)..releasedBy(arena);
44+
final jValue = JByteArray.from(value)..releasedBy(arena);
45+
_db.put(jKey, jValue);
46+
});
47+
}
48+
```
49+
50+
### 2. Idiomatic API Design and Type Handling
51+
52+
Raw bindings expose Java's conventions and require manual, repetitive type conversions. A wrapper class should expose a clean, Dart-like API.
53+
54+
**The Problem:** Java method names (`createIfMissing$1`) and types (`JString`, `JByteArray`) are exposed directly to the application code.
55+
56+
**The Solution:** Create a wrapper class that exposes methods with named parameters and standard Dart types (`String`, `Uint8List`), handling all the JNI conversions internally.
57+
58+
*The Improved API:*
59+
```dart
60+
static LevelDB open(String path, {bool createIfMissing = true}) { ... }
61+
void put(String key, String value) { ... }
62+
String? get(String key) { ... }
63+
```
64+
65+
This allows for clean, simple iteration in the application code:
66+
```dart
67+
for (var entry in db.entries) {
68+
print('${entry.key}, ${entry.value}');
69+
}
70+
```
71+
72+
### 3. JVM Initialization
73+
74+
The JVM is a process-level resource and should be initialized only once when the application starts.
75+
76+
**The Problem:** Calling `Jni.spawn()` inside library code.
77+
78+
**The Solution:** `Jni.spawn()` belongs in a locatio where it will be called once, like your application's `main()` function, not in the library. In this example, the library code should assume the JVM is already running.
79+
80+
*Correct Usage in `bin/jni_leveldb.dart`:*
81+
```dart
82+
void main(List<String> arguments) {
83+
// ... find JARs ...
84+
Jni.spawn(classPath: jars); // Spawn the JVM once.
85+
db(); // Run the application logic.
86+
}
87+
```
88+
89+
### The Final Result
90+
91+
By applying these principles, the application logic becomes simple, readable, and free of JNI-specific details.
92+
93+
*Final Application Code:*
94+
```dart
95+
import 'package:jni_leveldb/src/leveldb.dart';
96+
97+
void db() {
98+
final db = LevelDB.open('example.db');
99+
try {
100+
db.put('Akron', 'Ohio');
101+
db.put('Tampa', 'Florida');
102+
db.put('Cleveland', 'Ohio');
103+
104+
print('Tampa is in ${db.get('Tampa')}');
105+
106+
db.delete('Akron');
107+
108+
print('\nEntries in database:');
109+
for (var entry in db.entries) {
110+
print('${entry.key}, ${entry.value}');
111+
}
112+
} finally {
113+
db.close();
114+
}
115+
}
116+
```
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:jni_leveldb/jni_leveldb.dart' as jni_leveldb;
2+
import 'dart:io';
3+
import 'package:jni/jni.dart';
4+
5+
import 'package:path/path.dart';
6+
7+
const jarError = '';
8+
9+
void main(List<String> arguments) {
10+
11+
const jarDir = './mvn_jar/';
12+
List<String> jars;
13+
try {
14+
jars = Directory(jarDir)
15+
.listSync()
16+
.map((e) => e.path)
17+
.where((path) => path.endsWith('.jar'))
18+
.toList();
19+
} on OSError catch (_) {
20+
stderr.writeln(jarError);
21+
return;
22+
}
23+
if (jars.isEmpty) {
24+
stderr.writeln(jarError);
25+
return;
26+
}
27+
Jni.spawn(classPath: jars);
28+
29+
jni_leveldb.db();
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
output:
2+
dart:
3+
path: 'lib/leveldb/'
4+
5+
classes:
6+
- 'org.iq80.leveldb.DB'
7+
- 'org.iq80.leveldb.Options'
8+
- 'org.iq80.leveldb.DBIterator'
9+
- 'org.iq80.leveldb.impl.Iq80DBFactory'
10+
- 'org.iq80.leveldb.impl.SeekingIteratorAdapter'
11+
- 'java.io.File'
12+
13+
maven_downloads:
14+
source_deps:
15+
- 'org.iq80.leveldb:leveldb:0.12'
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:jni_leveldb/src/leveldb.dart';
2+
3+
void db() {
4+
final db = LevelDB.open('example.db');
5+
try {
6+
db.put('Akron', 'Ohio');
7+
db.put('Tampa', 'Florida');
8+
db.put('Cleveland', 'Ohio');
9+
db.put('Sunnyvale', 'California');
10+
11+
print('Tampa is in ${db.get('Tampa')}');
12+
13+
db.delete('Akron');
14+
15+
print('\nEntries in database:');
16+
for (var entry in db.entries) {
17+
print('${entry.key}, ${entry.value}');
18+
}
19+
} finally {
20+
db.close();
21+
}
22+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import 'dart:convert';
2+
import 'dart:typed_data';
3+
4+
import 'package:jni/jni.dart';
5+
import 'package:jni_leveldb/leveldb/java/io/File.dart' as java;
6+
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/DB.dart';
7+
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/Options.dart';
8+
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/impl/Iq80DBFactory.dart';
9+
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/impl/SeekingIteratorAdapter.dart';
10+
11+
class LevelDB {
12+
final DB _db;
13+
14+
LevelDB._(this._db);
15+
16+
static LevelDB open(String path, {bool createIfMissing = true}) {
17+
final options = Options()..createIfMissing$1(createIfMissing);
18+
final file = java.File(path.toJString());
19+
final db = Iq80DBFactory.factory!.open(file, options);
20+
if (db == null) {
21+
throw Exception('Failed to open database at $path');
22+
}
23+
return LevelDB._(db);
24+
}
25+
26+
void put(String key, String value) {
27+
putBytes(utf8.encode(key), utf8.encode(value));
28+
}
29+
30+
void putBytes(Uint8List key, Uint8List value) {
31+
using((arena) {
32+
final jKey = JByteArray.from(key)..releasedBy(arena);
33+
final jValue = JByteArray.from(value)..releasedBy(arena);
34+
_db.put(jKey, jValue);
35+
});
36+
}
37+
38+
String? get(String key) {
39+
final value = getBytes(utf8.encode(key));
40+
if (value == null) {
41+
return null;
42+
}
43+
return utf8.decode(value);
44+
}
45+
46+
Uint8List? getBytes(Uint8List key) {
47+
return using((arena) {
48+
final jKey = JByteArray.from(key)..releasedBy(arena);
49+
final value = _db.get(jKey);
50+
if (value == null) {
51+
return null;
52+
}
53+
final bytes = value.toList();
54+
value.release();
55+
return Uint8List.fromList(bytes);
56+
});
57+
}
58+
59+
void delete(String key) {
60+
deleteBytes(utf8.encode(key));
61+
}
62+
63+
void deleteBytes(Uint8List key) {
64+
using((arena) {
65+
final jKey = JByteArray.from(key)..releasedBy(arena);
66+
_db.delete(jKey);
67+
});
68+
}
69+
70+
void close() {
71+
_db.release();
72+
}
73+
74+
Iterable<MapEntry<String, String>> get entries sync* {
75+
final iterator = _db.iterator()?.as(SeekingIteratorAdapter.type);
76+
if (iterator == null) return;
77+
try {
78+
iterator.seekToFirst();
79+
while (iterator.hasNext()) {
80+
final entry = iterator.next();
81+
if (entry == null) continue;
82+
83+
final keyBytes = entry.getKey();
84+
final valueBytes = entry.getValue();
85+
86+
if (keyBytes == null || valueBytes == null) {
87+
keyBytes?.release();
88+
valueBytes?.release();
89+
entry.release();
90+
continue;
91+
}
92+
93+
final key = utf8.decode(keyBytes.toList());
94+
final value = utf8.decode(valueBytes.toList());
95+
96+
keyBytes.release();
97+
valueBytes.release();
98+
entry.release();
99+
100+
yield MapEntry(key, value);
101+
}
102+
} finally {
103+
iterator.release();
104+
}
105+
}
106+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: jni_leveldb
2+
description: A sample command-line application.
3+
version: 1.0.0
4+
# repository: https://github.com/my_org/my_repo
5+
6+
publish_to: none
7+
8+
environment:
9+
sdk: ^3.6.2
10+
11+
# Add regular dependencies here.
12+
dependencies:
13+
jnigen:
14+
path: ../native-non-fork/pkgs/jnigen
15+
jni: ^0.14.1
16+
17+
dev_dependencies:
18+
lints: ^5.0.0
19+
test: ^1.24.0

0 commit comments

Comments
 (0)