Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 jnigen_db_native_interop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/mvn_java
/.dart_tool
.DS_Store
/.idea
/build
28 changes: 28 additions & 0 deletions jnigen_db_native_interop/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
BSD 3-Clause License

Copyright (c) 2025, James Williams

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
116 changes: 116 additions & 0 deletions jnigen_db_native_interop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# jni_leveldb

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`.

## Prerequisites

- Java Development Kit (JDK) version 1.8 or later must be installed, and the `JAVA_HOME` environment variable must be set to its location.

## Setup and Running

1. **Generate Bindings and Build JNI code:**

Run `jnigen` to download the required Java libraries, generate Dart bindings, and build the JNI glue code.

```bash
dart run jni:setup
dart run jnigen:setup
dart run jnigen --config jnigen.yaml
```

2. **Run the application:**

```bash
dart run bin/jni_leveldb.dart
```

## From Raw Bindings to an Idiomatic Dart API

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.

### 1. Resource Management

JNI objects are handles to resources in the JVM. Failing to release them causes memory leaks.

**The Problem:** Forgetting to call `release()` on JNI objects.

**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()`.

*Example from the wrapper:*
```dart
void putBytes(Uint8List key, Uint8List value) {
using((arena) {
final jKey = JByteArray.from(key)..releasedBy(arena);
final jValue = JByteArray.from(value)..releasedBy(arena);
_db.put(jKey, jValue);
});
}
```

### 2. Idiomatic API Design and Type Handling

Raw bindings expose Java's conventions and require manual, repetitive type conversions. A wrapper class should expose a clean, Dart-like API.

**The Problem:** Java method names (`createIfMissing$1`) and types (`JString`, `JByteArray`) are exposed directly to the application code.

**The Solution:** Create a wrapper class that exposes methods with named parameters and standard Dart types (`String`, `Uint8List`), handling all the JNI conversions internally.

*The Improved API:*
```dart
static LevelDB open(String path, {bool createIfMissing = true}) { ... }
void put(String key, String value) { ... }
String? get(String key) { ... }
```

This allows for clean, simple iteration in the application code:
```dart
for (var entry in db.entries) {
print('${entry.key}, ${entry.value}');
}
```

### 3. JVM Initialization

The JVM is a process-level resource and should be initialized only once when the application starts.

**The Problem:** Calling `Jni.spawn()` inside library code.

**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.

*Correct Usage in `bin/jni_leveldb.dart`:*
```dart
void main(List<String> arguments) {
// ... find JARs ...
Jni.spawn(classPath: jars); // Spawn the JVM once.
db(); // Run the application logic.
}
```

### The Final Result

By applying these principles, the application logic becomes simple, readable, and free of JNI-specific details.

*Final Application Code:*
```dart
import 'package:jni_leveldb/src/leveldb.dart';

void db() {
final db = LevelDB.open('example.db');
try {
db.put('Akron', 'Ohio');
db.put('Tampa', 'Florida');
db.put('Cleveland', 'Ohio');

print('Tampa is in ${db.get('Tampa')}');

db.delete('Akron');

print('\nEntries in database:');
for (var entry in db.entries) {
print('${entry.key}, ${entry.value}');
}
} finally {
db.close();
}
}
```
30 changes: 30 additions & 0 deletions jnigen_db_native_interop/bin/jni_leveldb.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'package:jni_leveldb/jni_leveldb.dart' as jni_leveldb;
import 'dart:io';
import 'package:jni/jni.dart';

import 'package:path/path.dart';

const jarError = '';

void main(List<String> arguments) {

const jarDir = './mvn_jar/';
List<String> jars;
try {
jars = Directory(jarDir)
.listSync()
.map((e) => e.path)
.where((path) => path.endsWith('.jar'))
.toList();
} on OSError catch (_) {
stderr.writeln(jarError);
return;
}
if (jars.isEmpty) {
stderr.writeln(jarError);
return;
}
Jni.spawn(classPath: jars);

jni_leveldb.db();
}
15 changes: 15 additions & 0 deletions jnigen_db_native_interop/jnigen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
output:
dart:
path: 'lib/leveldb/'

classes:
- 'org.iq80.leveldb.DB'
- 'org.iq80.leveldb.Options'
- 'org.iq80.leveldb.DBIterator'
- 'org.iq80.leveldb.impl.Iq80DBFactory'
- 'org.iq80.leveldb.impl.SeekingIteratorAdapter'
- 'java.io.File'

maven_downloads:
source_deps:
- 'org.iq80.leveldb:leveldb:0.12'
22 changes: 22 additions & 0 deletions jnigen_db_native_interop/lib/jni_leveldb.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:jni_leveldb/src/leveldb.dart';

void db() {
final db = LevelDB.open('example.db');
try {
db.put('Akron', 'Ohio');
db.put('Tampa', 'Florida');
db.put('Cleveland', 'Ohio');
db.put('Sunnyvale', 'California');

print('Tampa is in ${db.get('Tampa')}');

db.delete('Akron');

print('\nEntries in database:');
for (var entry in db.entries) {
print('${entry.key}, ${entry.value}');
}
} finally {
db.close();
}
}
106 changes: 106 additions & 0 deletions jnigen_db_native_interop/lib/src/leveldb.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:jni/jni.dart';
import 'package:jni_leveldb/leveldb/java/io/File.dart' as java;
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/DB.dart';
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/Options.dart';
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/impl/Iq80DBFactory.dart';
import 'package:jni_leveldb/leveldb/org/iq80/leveldb/impl/SeekingIteratorAdapter.dart';

class LevelDB {
final DB _db;

LevelDB._(this._db);

static LevelDB open(String path, {bool createIfMissing = true}) {
final options = Options()..createIfMissing$1(createIfMissing);
final file = java.File(path.toJString());
final db = Iq80DBFactory.factory!.open(file, options);
if (db == null) {
throw Exception('Failed to open database at $path');
}
return LevelDB._(db);
}

void put(String key, String value) {
putBytes(utf8.encode(key), utf8.encode(value));
}

void putBytes(Uint8List key, Uint8List value) {
using((arena) {
final jKey = JByteArray.from(key)..releasedBy(arena);
final jValue = JByteArray.from(value)..releasedBy(arena);
_db.put(jKey, jValue);
});
}

String? get(String key) {
final value = getBytes(utf8.encode(key));
if (value == null) {
return null;
}
return utf8.decode(value);
}

Uint8List? getBytes(Uint8List key) {
return using((arena) {
final jKey = JByteArray.from(key)..releasedBy(arena);
final value = _db.get(jKey);
if (value == null) {
return null;
}
final bytes = value.toList();
value.release();
return Uint8List.fromList(bytes);
});
}

void delete(String key) {
deleteBytes(utf8.encode(key));
}

void deleteBytes(Uint8List key) {
using((arena) {
final jKey = JByteArray.from(key)..releasedBy(arena);
_db.delete(jKey);
});
}

void close() {
_db.release();
}

Iterable<MapEntry<String, String>> get entries sync* {
final iterator = _db.iterator()?.as(SeekingIteratorAdapter.type);
if (iterator == null) return;
try {
iterator.seekToFirst();
while (iterator.hasNext()) {
final entry = iterator.next();
if (entry == null) continue;

final keyBytes = entry.getKey();
final valueBytes = entry.getValue();

if (keyBytes == null || valueBytes == null) {
keyBytes?.release();
valueBytes?.release();
entry.release();
continue;
}

final key = utf8.decode(keyBytes.toList());
final value = utf8.decode(valueBytes.toList());

keyBytes.release();
valueBytes.release();
entry.release();

yield MapEntry(key, value);
}
} finally {
iterator.release();
}
}
}
19 changes: 19 additions & 0 deletions jnigen_db_native_interop/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: jni_leveldb
description: A sample command-line application.
version: 1.0.0
# repository: https://github.com/my_org/my_repo

publish_to: none

environment:
sdk: ^3.6.2

# Add regular dependencies here.
dependencies:
jnigen:
path: ../native-non-fork/pkgs/jnigen
jni: ^0.14.1

dev_dependencies:
lints: ^5.0.0
test: ^1.24.0
Loading