Skip to content

Commit 3849808

Browse files
[jnigen] Improve the UX of Jni.spawn for Dart standalone (#1916)
* Clarify setup for jnigen on Windows - Add section for generating java types * Do not require setting `dylibDir` explicitly when running `Jni.spawn` * Improve docs and error messages Co-authored-by: Hossein Yousefi <[email protected]>
1 parent 3ed5d3d commit 3849808

File tree

8 files changed

+72
-23
lines changed

8 files changed

+72
-23
lines changed

pkgs/jni/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 0.14.0-wip
2+
3+
- Added `DynamicLibraryLoadError` which is thrown when the dynamic library fails
4+
to load. `HelperNotFoundError` will only be thrown when the helper library
5+
cannot be found.
6+
- Update the README.md to include info about generating bindings for built-in
7+
java types.
8+
- Do not require a `dylibDir` when running `Jni.spawn` from Dart standalone,
9+
instead use the default value of `build/jni_libs`.
10+
111
## 0.13.0
212

313
- **Breaking Change**: Separated primitive arrays from object arrays.

pkgs/jni/bin/setup.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const jniNativeBuildDirective =
1212
'# jni_native_build (Build with jni:setup. Do not delete this line.)';
1313

1414
// When changing this constant here, also change corresponding path in
15-
// test/test_util.
15+
// test/test_util, and the default value for `Jni._dylibDir`.
1616
const _defaultRelativeBuildPath = 'build/jni_libs';
1717

1818
const _buildPath = 'build-path';

pkgs/jni/lib/src/errors.dart

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'dart:io';
6+
57
import 'third_party/generated_bindings.dart';
68

79
// TODO(#567): Add the fact that [JException] is now a [JObject] to the
@@ -100,9 +102,30 @@ final class HelperNotFoundError extends Error {
100102

101103
@override
102104
String toString() => '''
103-
Lookup for helper library $path failed.
104-
Please ensure that `dartjni` shared library is built.
105-
Provided jni:setup script can be used to build the shared library.
106-
If the library is already built, ensure that the JVM libraries can be
107-
loaded from Dart.''';
105+
Unable to locate the helper library.
106+
107+
Ensure that the helper library is available at the path: $path
108+
Run `dart jni:setup` to generate the shared library if it does not exist.
109+
110+
Note: If the --build-path option is passed to jni:setup, Jni.spawn must be
111+
called with same dylibDir. Also when creating new Dart isolates, Jni.setDylibDir
112+
must be called.
113+
''';
114+
}
115+
116+
final class DynamicLibraryLoadError extends Error {
117+
final String libraryPath;
118+
119+
DynamicLibraryLoadError(this.libraryPath);
120+
121+
@override
122+
String toString() {
123+
return '''
124+
Failed to load dynamic library at path: $libraryPath
125+
The library was found at the specified path, but it could not be loaded.
126+
This might be due to missing dependencies or incorrect file permissions.
127+
Please ensure ${Platform.isWindows ? r'that `\bin\server\jvm.dll` is in the PATH, and ' : ''}that the file has the correct permissions.
128+
129+
''';
130+
}
108131
}

pkgs/jni/lib/src/jni.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ String _getLibraryFileName(String base) {
3636
DynamicLibrary _loadDartJniLibrary({String? dir, String baseName = 'dartjni'}) {
3737
final fileName = _getLibraryFileName(baseName);
3838
final libPath = (dir != null) ? join(dir, fileName) : fileName;
39+
final file = File(libPath);
40+
if (!file.existsSync()) {
41+
throw HelperNotFoundError(libPath);
42+
}
3943
try {
4044
final dylib = DynamicLibrary.open(libPath);
4145
return dylib;
4246
} catch (_) {
43-
throw HelperNotFoundError(libPath);
47+
throw DynamicLibraryLoadError(libPath);
4448
}
4549
}
4650

@@ -50,7 +54,7 @@ abstract final class Jni {
5054
static final JniBindings _bindings = JniBindings(_dylib);
5155

5256
/// Store dylibDir if any was used.
53-
static String? _dylibDir;
57+
static String _dylibDir = join('build', 'jni_libs');
5458

5559
/// Sets the directory where dynamic libraries are looked for.
5660
/// On dart standalone, call this in new isolate before doing
@@ -108,7 +112,7 @@ abstract final class Jni {
108112
int jniVersion = JniVersions.JNI_VERSION_1_6,
109113
}) =>
110114
using((arena) {
111-
_dylibDir = dylibDir;
115+
_dylibDir = dylibDir ?? _dylibDir;
112116
final jvmArgs = _createVMArgs(
113117
options: jvmOptions,
114118
classPath: classPath,

pkgs/jni/pubspec.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
name: jni
66
description: A library to access JNI from Dart and Flutter that acts as a support library for package:jnigen.
7-
version: 0.13.0
7+
version: 0.14.0-wip
88
repository: https://github.com/dart-lang/native/tree/main/pkgs/jni
99
issue_tracker: https://github.com/dart-lang/native/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ajni
1010

@@ -16,8 +16,8 @@ topics:
1616
- jni
1717

1818
environment:
19-
sdk: '>=3.3.0 <4.0.0'
20-
flutter: '>=2.11.0'
19+
sdk: ">=3.3.0 <4.0.0"
20+
flutter: ">=2.11.0"
2121

2222
dependencies:
2323
args: ^2.5.0

pkgs/jni/test/isolate_test.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ void run({required TestRunnerCallback testRunner}) {
2424
final foo = 'foo'.toJString();
2525
final port = ReceivePort();
2626
await Isolate.spawn((sendPort) {
27-
Jni.setDylibDir(dylibDir: 'build/jni_libs');
2827
sendPort.send(foo.toDartString());
2928
Isolate.current.kill();
3029
}, port.sendPort);
@@ -37,7 +36,6 @@ void run({required TestRunnerCallback testRunner}) {
3736
// isolates.
3837
'foo'.toJString();
3938
await Isolate.spawn((_) {
40-
Jni.setDylibDir(dylibDir: 'build/jni_libs');
4139
'bar'.toJString();
4240
Isolate.current.kill();
4341
}, null);

pkgs/jni/test/jobject_test.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -225,12 +225,6 @@ void run({required TestRunnerCallback testRunner}) {
225225
testRunner('Isolate', () async {
226226
final receivePort = ReceivePort();
227227
await Isolate.spawn((sendPort) {
228-
// On standalone target, make sure to call [setDylibDir] before accessing
229-
// any JNI function in a new isolate.
230-
//
231-
// otherwise subsequent JNI calls will throw a "library not found"
232-
// exception.
233-
Jni.setDylibDir(dylibDir: 'build/jni_libs');
234228
final randomClass = JClass.forName('java/util/Random');
235229
final random =
236230
randomClass.constructorId('()V').call(randomClass, JObject.type, []);

pkgs/jnigen/README.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ More advanced features such as callbacks are not supported yet. Support for thes
135135

136136
On Flutter targets, native libraries are built automatically and bundled. On standalone platforms, no such infrastructure exists yet. As a stopgap solution, running `dart run jni:setup` in a target directory builds all JNI native dependencies of the package into `build/jni_libs`.
137137

138-
The build directory has to be passed to `Jni.spawn` call. It's assumed that all dependencies are built into the same target directory, so that once JNI is initialized, generated bindings can load their respective C libraries automatically.
138+
To start a JVM, call `Jni.spawn`. It's assumed that all dependencies are built into the same target directory, so that once JNI is initialized, generated bindings can load their respective C libraries automatically.
139+
140+
If a custom build path has been set for the dynamic libraries built by `dart run jni:setup --build-path path/to/dylib`, the same path must be passed to `Jni.spawn`. Also anytime a new Dart isolate is spawned, the directory must be set again using `Jni.setDylibDir`.
139141

140142
## Requirements
141143
### SDK
@@ -154,9 +156,11 @@ __On windows, append the path of `jvm.dll` in your JDK installation to PATH.__
154156
For example, on Powershell:
155157

156158
```powershell
157-
$env:Path += ";${env:JAVA_HOME}\bin\server".
159+
$env:Path += ";${env:JAVA_HOME}\bin\server"
158160
```
159161

162+
Note: The above will only add `jvm.dll` to `PATH` for the current Powershell session, use the Control Panel to add it to path permanently.
163+
160164
If JAVA_HOME not set, find the `java.exe` executable and set the environment variable in Control Panel. If java is installed through a package manager, there may be a more automatic way to do this. (Eg: `scoop reset`).
161165

162166
### C tooling
@@ -179,6 +183,22 @@ If the classes are in JAR file, make sure to provide path to JAR file itself, an
179183
#### `jnigen` is unable to parse sources.
180184
If the errors are similar to `symbol not found`, ensure all dependencies of the source are available. If such dependency is compiled, it can be included in `class_path`.
181185

186+
#### Generate bindings for built-in types
187+
To generate bindings for built-in Java types (like those in `java.*` packages), you need to provide the OpenJDK source code to the generator. Here's how:
188+
189+
1. Locate the `src.zip` file in your Java installation directory
190+
2. Extract the `java.base` folder from this zip file
191+
3. Add the path to the extracted `java.base` folder in your `source_path` list in the YAML configuration
192+
193+
For example, to generate bindings for `java.lang.Math`, your configuration might look like:
194+
195+
```yaml
196+
source_path:
197+
- '/path/to/extracted/java.base'
198+
classes:
199+
- 'java.lang.Math'
200+
```
201+
182202
#### How are classes mapped into bindings?
183203
Each Java class generates a subclass of `JObject` class, which wraps a `jobject` reference in JNI. Nested classes use `_` as separator, `Example.NestedClass` will be mapped to `Example_NestedClass`.
184204

@@ -248,7 +268,7 @@ Currently we don't have an automatic mechanism for using these. You can unpack t
248268
However there are 2 caveats to this caveat.
249269
250270
* SDK stubs after version 28 are incomplete. OpenJDK Doclet API we use to generate API summaries will error on incomplete sources.
251-
* The API can't process the `java.**` namespaces in the Android SDK stubs, because it expects a module layout. So if you want to generate bindings for, say, `java.lang.Math`, you cannot use the Android SDK stubs. OpenJDK sources can be used instead.
271+
* The API can't process the `java.**` namespaces in the Android SDK stubs, because it expects a module layout. So if you want to generate bindings for, say, `java.lang.Math`, you cannot use the Android SDK stubs. OpenJDK sources can be used instead. See [Generate bindings for built-in types](#generate-bindings-for-built-in-types) above for instructions on how to use OpenJDK sources.
252272
253273
The JAR files (`$SDK_ROOT/platforms/android-$VERSION/android.jar`) can be used instead. But compiled JARs do not include JavaDoc and method parameter names. This JAR is automatically included by Gradle when `android_sdk_config` >> `add_gradle_deps` is specified.
254274

0 commit comments

Comments
 (0)