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
46 changes: 46 additions & 0 deletions jme3-core/src/main/java/com/jme3/util/PreserveReflection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2009-2026 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * 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.
*
* * Neither the name of 'jMonkeyEngine' 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 OWNER 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.
*/
package com.jme3.util;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks a class whose constructors, fields, and methods should be preserved for
* reflective access by ahead-of-time compilation and metadata generation tools.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PreserveReflection {
}
151 changes: 151 additions & 0 deletions jme3-nativeimage-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# jMonkeyEngine Native Image Gradle Plugin

The `org.jmonkeyengine.nativeimage` plugin generates GraalVM Native Image
reachability metadata for jMonkeyEngine applications and prepares the native
runtime library layout used by native-image executables.

Use it together with the official GraalVM Build Tools plugin:

```groovy
plugins {
id 'application'
id 'org.graalvm.buildtools.native' version '1.1.0'
id 'org.jmonkeyengine.nativeimage'
}

application {
mainClass = 'com.example.MyGame'
}

graalvmNative {
binaries {
named('main') {
imageName = 'my-game'
mainClass = 'com.example.MyGame'
}
}
}

jmeNativeImage {
useDefaultResourceSettings()
}
```

Build the native executable with:

```bash
./gradlew nativeCompile
```

## Reflection Metadata

The plugin automatically preserves common jME runtime types, such as
`Savable`, `AssetLoader`, `Control`, `Filter`, networking serializers, and
other engine extension points.

For application classes, use the `jmeNativeImage` DSL:

```groovy
jmeNativeImage {
targetType 'com.example.MyNiftyController'
targetType 'com.example.MyCustomAssetLoader'
}
```

`targetType` accepts either a concrete class or a type used as a match target.
If the configured type is an interface or superclass, the generator preserves
matching classes found on the scan classpath.

You can also preserve all classes marked with an annotation:

```groovy
jmeNativeImage {
targetAnnotation 'com.example.ReflectiveEntryPoint'
}
```

jME also provides a built-in annotation for this:

```java
import com.jme3.util.PreserveReflection;

@PreserveReflection
public class MyNiftyController {
public void startGame() {
}
}
```

Classes annotated with `@PreserveReflection` are included by default when this
plugin generates metadata.


## Default Resource Settings

```groovy
jmeNativeImage {
useDefaultResourceSettings()
}
```

adds broad default GraalVM resource settings, compatible with typical jME applications.

You can override those defaults through the DSL:

```groovy
jmeNativeImage {
includeResources 'Interface/.*|Textures/.*|Models/.*'
excludeResources '(?i).*\\.(class|jar|dylib|so|dll|jnilib)$'
}
```


## Advanced Usage

The following additional options are available, but most applications
should not need them. The plugin already scans the main source set resources,
preserves common jME extension points, and supports `@PreserveReflection` for
application classes.

### Resource Globs

A resource glob is an optional path pattern for extra files that should be
included as resources in the generated native image metadata.

It is not a Java class name. It is a resource path inside your application or
dependency jars, using `/` separators.

You usually do not need to configure resource globs for ordinary files under
the project's `src/main/resources`: the plugin scans the main source set
resources and writes metadata for them automatically.

Use `resourceGlob` when a resource is outside the normal main resources scan,
comes from a dependency, is generated by a separate task, or needs to be added
as a broader pattern instead of as a discovered concrete file.

Examples of optional extra resource globs:

```groovy
jmeNativeImage {
resourceGlob 'Interface/**'
resourceGlob 'Textures/**'
resourceGlob 'Models/**/*.j3o'
resourceGlob 'com/example/external-runtime-config.properties'
}
```

### Proxy Interfaces

If your application uses dynamic proxies, configure the interface set:

```groovy
jmeNativeImage {
proxyInterfaceSet([
'com.example.Service',
'com.example.ServiceEvents'
])
}
```

Each `proxyInterfaceSet` entry represents one proxy definition containing the
interfaces implemented by that proxy.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.jmonkeyengine.gradle.nativeimage

import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property

import javax.inject.Inject

/**
* DSL extension for jMonkeyEngine Native Image metadata generation.
*/
class JmeNativeImageExtension {

final ListProperty<String> additionalTargetTypes
final ListProperty<String> additionalTargetAnnotations
final ListProperty<List<String>> additionalProxyInterfaceSets
final ListProperty<String> additionalResourceGlobs
final Property<String> includeResourcesPattern
final Property<String> excludeResourcesPattern
final Property<Boolean> defaultResourceSettings

@Inject
JmeNativeImageExtension(ObjectFactory objects) {
additionalTargetTypes = objects.listProperty(String).convention([])
additionalTargetAnnotations = objects.listProperty(String).convention([])
additionalProxyInterfaceSets = objects.listProperty(List).convention([])
additionalResourceGlobs = objects.listProperty(String).convention([])
includeResourcesPattern = objects.property(String)
excludeResourcesPattern = objects.property(String)
defaultResourceSettings = objects.property(Boolean).convention(false)
}

void targetType(String className) {
additionalTargetTypes.add(className)
}

void targetAnnotation(String annotationClassName) {
additionalTargetAnnotations.add(annotationClassName)
}

void proxyInterfaceSet(Iterable<String> interfaceClassNames) {
additionalProxyInterfaceSets.add(interfaceClassNames.toList())
}
Comment on lines +41 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The method signature accepts Iterable<String>, but ArrayList does not have a constructor that accepts a raw Iterable (it only accepts Collection). If a user passes an Iterable that does not implement Collection (such as certain Gradle lazy providers or custom iterables), this will throw a MissingConstructorException at runtime.

Using Groovy's toList() extension method on Iterable is safer and more idiomatic.

    void proxyInterfaceSet(Iterable<String> interfaceClassNames) {
        additionalProxyInterfaceSets.add(interfaceClassNames.toList())
    }


void resourceGlob(String glob) {
additionalResourceGlobs.add(glob)
}

void includeResources(String pattern) {
includeResourcesPattern.set(pattern)
}

void excludeResources(String pattern) {
excludeResourcesPattern.set(pattern)
}

void useDefaultResourceSettings() {
defaultResourceSettings.set(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class JmeNativeImagePlugin implements Plugin<Project> {

@Override
void apply(Project project) {
project.extensions.create('jmeNativeImage', JmeNativeImageExtension)
URL metadataScript = getClass().getResource('/org/jmonkeyengine/gradle/nativeimage/native-image-metadata.gradle')
if (metadataScript == null) {
throw new IllegalStateException('Unable to locate bundled native image Gradle script.')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import groovy.json.JsonSlurper
import org.gradle.api.tasks.PathSensitivity

def extraProperties = project.extensions.extraProperties
def nativeImageExtension = project.extensions.findByName('jmeNativeImage')
[
'jmeNativeImageAdditionalTargetTypes',
'jmeNativeImageAdditionalTargetAnnotations',
Expand All @@ -28,12 +29,23 @@ if (!extraProperties.has('jmeApplyDefaultNativeImageResourceSettings')) {
return
}

String includeResourcesPattern = project.ext.jmeNativeImageIncludeResourcesPattern?.toString() ?: '.*'
String excludeResourcesPattern = project.ext.jmeNativeImageExcludeResourcesPattern?.toString() ?: '(?i).*\\.(class|jar|dylib|so|dll|jnilib)$'
String includeResourcesPattern = nativeImageExtension?.includeResourcesPattern?.isPresent()
? nativeImageExtension.includeResourcesPattern.get()
: project.ext.jmeNativeImageIncludeResourcesPattern?.toString() ?: '.*'
String excludeResourcesPattern = nativeImageExtension?.excludeResourcesPattern?.isPresent()
? nativeImageExtension.excludeResourcesPattern.get()
: project.ext.jmeNativeImageExcludeResourcesPattern?.toString() ?: '(?i).*\\.(class|jar|dylib|so|dll|jnilib)$'

nativeBinary.resources.autodetect()
nativeBinary.buildArgs.add("-H:IncludeResources=${includeResourcesPattern}")
nativeBinary.buildArgs.add("-H:ExcludeResources=${excludeResourcesPattern}")
List<String> currentBuildArgs = nativeBinary.buildArgs.getOrElse([])
String includeArg = "-H:IncludeResources=${includeResourcesPattern}".toString()
String excludeArg = "-H:ExcludeResources=${excludeResourcesPattern}".toString()
if (!currentBuildArgs.contains(includeArg)) {
nativeBinary.buildArgs.add(includeArg)
}
if (!currentBuildArgs.contains(excludeArg)) {
nativeBinary.buildArgs.add(excludeArg)
}
})
}

Expand Down Expand Up @@ -67,6 +79,7 @@ def defaultTargetTypes = [
]

def defaultTargetAnnotations = [
'com.jme3.util.PreserveReflection',
'com.jme3.network.serializing.Serializable'
]

Expand Down Expand Up @@ -122,6 +135,22 @@ def asStringMatrix = { Object value ->
}.findAll { !it.isEmpty() }
}

def extensionPropertyValue = { String propertyName ->
if (nativeImageExtension == null || !nativeImageExtension.hasProperty(propertyName)) {
return []
}
def property = nativeImageExtension[propertyName]
return property?.getOrElse([])
}

def configuredStringList = { String extraPropertyName, String extensionPropertyName ->
return asStringList(project.ext[extraPropertyName]) + asStringList(extensionPropertyValue(extensionPropertyName))
}

def configuredStringMatrix = { String extraPropertyName, String extensionPropertyName ->
return asStringMatrix(project.ext[extraPropertyName]) + asStringMatrix(extensionPropertyValue(extensionPropertyName))
}

def loadClassSafely = { ClassLoader loader, String className ->
try {
return Class.forName(className, false, loader)
Expand Down Expand Up @@ -332,27 +361,27 @@ def isJdkClassName = { String className ->
}

def normalizedTargetTypes = {
return (defaultTargetTypes + asStringList(project.ext.jmeNativeImageAdditionalTargetTypes))
return (defaultTargetTypes + configuredStringList('jmeNativeImageAdditionalTargetTypes', 'additionalTargetTypes'))
.findAll { !it.isEmpty() }
.toSorted()
.unique()
}

def normalizedTargetAnnotations = {
return (defaultTargetAnnotations + asStringList(project.ext.jmeNativeImageAdditionalTargetAnnotations))
return (defaultTargetAnnotations + configuredStringList('jmeNativeImageAdditionalTargetAnnotations', 'additionalTargetAnnotations'))
.findAll { !it.isEmpty() }
.toSorted()
.unique()
}

def normalizedProxyInterfaceSets = {
return (defaultProxyInterfaceSets + asStringMatrix(project.ext.jmeNativeImageAdditionalProxyInterfaceSets)).collect { interfaceSet ->
return (defaultProxyInterfaceSets + configuredStringMatrix('jmeNativeImageAdditionalProxyInterfaceSets', 'additionalProxyInterfaceSets')).collect { interfaceSet ->
new ArrayList<>(new LinkedHashSet<>(interfaceSet))
}.findAll { !it.isEmpty() }
}

def normalizedResourceGlobs = {
return asStringList(project.ext.jmeNativeImageAdditionalResourceGlobs)
return configuredStringList('jmeNativeImageAdditionalResourceGlobs', 'additionalResourceGlobs')
.findAll { !it.isEmpty() }
.toSorted()
.unique()
Expand Down Expand Up @@ -535,9 +564,9 @@ project.afterEvaluate {
}
}

def additionalTargetTypes = asStringList(project.ext.jmeNativeImageAdditionalTargetTypes).findAll { !it.isEmpty() }.toSorted().unique()
def additionalTargetAnnotations = asStringList(project.ext.jmeNativeImageAdditionalTargetAnnotations).findAll { !it.isEmpty() }.toSorted().unique()
def additionalProxyInterfaceSets = asStringMatrix(project.ext.jmeNativeImageAdditionalProxyInterfaceSets).collect { interfaceSet ->
def additionalTargetTypes = configuredStringList('jmeNativeImageAdditionalTargetTypes', 'additionalTargetTypes').findAll { !it.isEmpty() }.toSorted().unique()
def additionalTargetAnnotations = configuredStringList('jmeNativeImageAdditionalTargetAnnotations', 'additionalTargetAnnotations').findAll { !it.isEmpty() }.toSorted().unique()
def additionalProxyInterfaceSets = configuredStringMatrix('jmeNativeImageAdditionalProxyInterfaceSets', 'additionalProxyInterfaceSets').collect { interfaceSet ->
new ArrayList<>(new LinkedHashSet<>(interfaceSet))
}.findAll { !it.isEmpty() }
def targetTypes = normalizedTargetTypes()
Expand Down Expand Up @@ -969,6 +998,11 @@ project.afterEvaluate {
if (!currentJvmArgs.contains(nativeImageRuntimeSupportExportArg)) {
nativeBinary.jvmArgs.add(nativeImageRuntimeSupportExportArg)
}
if (nativeImageExtension?.defaultResourceSettings?.getOrElse(false)
|| nativeImageExtension?.includeResourcesPattern?.isPresent()
|| nativeImageExtension?.excludeResourcesPattern?.isPresent()) {
project.ext.jmeApplyDefaultNativeImageResourceSettings(nativeBinary)
}
}
}
}
Expand Down
Loading