diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f02d203..8f7a3de 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,14 +1,28 @@
-name: Build the examples
+name: CI
on:
push:
branches:
- master
pull_request:
- branches:
- - master
jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: ruff check
+ uses: astral-sh/ruff-action@v3
+ with:
+ args: check
+
+ - name: ruff format
+ uses: astral-sh/ruff-action@v3
+ with:
+ args: format --check
+
build:
runs-on: ubuntu-latest
@@ -34,7 +48,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
+ python-version: ["3.12", "3.13", "3.14"]
steps:
- name: Checkout repository
@@ -46,12 +60,15 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install giws
- run: python -m pip install .
+ run: python -m pip install . pytest
+
+ - name: Run the test suite
+ run: python -m pytest
- name: Smoke-test the installed script
run: |
giws --version
cd examples/basic_example
- giws -f MyComplexClass.giws.xml
+ giws -p -f MyComplexClass.giws.xml
test -f basic_example.cpp
test -f basic_example.hxx
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
deleted file mode 100644
index acf81a7..0000000
--- a/.github/workflows/lint.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-name: Lint
-
-on: [push, pull_request]
-
-jobs:
- ruff:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: astral-sh/ruff-action@v3
- with:
- args: check
- - uses: astral-sh/ruff-action@v3
- with:
- args: format --check
diff --git a/CHANGELOG b/CHANGELOG
index eebc337..04d291c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,6 +11,12 @@ giws (3.1.0)
--body-extension-file as intended
* Fix the error message raised for an unknown datatype
* CI: update actions, add a Python 3.9-3.14 install/smoke-test matrix
+ * Generated GiwsException: check for pending exceptions after each
+ CallObjectMethod (fixes -Xcheck:jni warnings while building the
+ C++ exception)
+ * Add a test suite (tests/) run by CI; no JDK needed
+ * New example bug_nonstatic_exception demonstrating the exception
+ fix on non-static methods
* Modernize the Python code (f-strings, type hints, with-statement)
* Add a pre-commit configuration running ruff
diff --git a/CXXException.py b/CXXException.py
index 5a0c684..bb387d9 100644
--- a/CXXException.py
+++ b/CXXException.py
@@ -410,6 +410,12 @@ def generateCXXBody(self, config):
// call getLocalizedMessage
jstring description = (jstring) curEnv->CallObjectMethod(javaException, getLocalizedMessageId);
+ if (curEnv->ExceptionCheck())
+ {
+ curEnv->ExceptionClear();
+ return "";
+ }
+
if (description == NULL)
{
return "";
@@ -442,6 +448,12 @@ def generateCXXBody(self, config):
// call getStackTrace
jobjectArray stackTrace = (jobjectArray) curEnv->CallObjectMethod(javaException, getStackTraceId);
+ if (curEnv->ExceptionCheck())
+ {
+ curEnv->ExceptionClear();
+ return "";
+ }
+
if (stackTrace == NULL)
{
return "";
@@ -463,6 +475,15 @@ def generateCXXBody(self, config):
// call to string on the object
jstring stackElementString = (jstring) curEnv->CallObjectMethod(curStackTraceElement, toStringId);
+ if (curEnv->ExceptionCheck())
+ {
+ curEnv->ExceptionClear();
+ curEnv->DeleteLocalRef(stackTraceElementClass);
+ curEnv->DeleteLocalRef(stackTrace);
+ curEnv->DeleteLocalRef(curStackTraceElement);
+ return res;
+ }
+
if (stackElementString == NULL)
{
curEnv->DeleteLocalRef(stackTraceElementClass);
@@ -505,6 +526,14 @@ def generateCXXBody(self, config):
// call the getName function
jstring javaName = (jstring) curEnv->CallObjectMethod(exceptionClass, getNameId);
+ if (curEnv->ExceptionCheck())
+ {
+ curEnv->ExceptionClear();
+ curEnv->DeleteLocalRef(exceptionClass);
+ curEnv->DeleteLocalRef(classClass);
+ return "";
+ }
+
if (javaName == NULL)
{
return "";
diff --git a/datatypes/dataGiws.py b/datatypes/dataGiws.py
index 6d3c880..00e8326 100644
--- a/datatypes/dataGiws.py
+++ b/datatypes/dataGiws.py
@@ -38,11 +38,6 @@
from JNIFrameWork import JNIFrameWork
-def abstractMethod(obj=None):
- """Use this instead of 'pass' for the body of abstract methods."""
- raise Exception(f"Unimplemented abstract method: {obj}")
-
-
#
# This class intend to create a generic object for datatype
# see http://en.wikipedia.org/wiki/Java_Native_Interface#Mapping_types
@@ -126,11 +121,11 @@ def getCallStaticMethod(self):
def getRealJavaType(self):
"""Returns the real datatype"""
- abstractMethod(self)
+ raise NotImplementedError(type(self).__name__)
def getDescription(self):
"""Returns the description"""
- abstractMethod(self)
+ raise NotImplementedError(type(self).__name__)
def setIsArray(self, isItAnArray):
"""Defines if we have to deal with an array or not"""
diff --git a/examples/Makefile b/examples/Makefile
index e304dd4..ae3d62c 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -9,7 +9,7 @@ SHELL = /bin/sh
# list of buildable examples
EX = basic_example example1 example2 example3 example4 example5 inherit bytebuffer \
-bug_no_param_int_array bug_disable_return bug_string_array_len
+bug_no_param_int_array bug_disable_return bug_string_array_len bug_nonstatic_exception
# verify that JAVA_HOME is set to something (should be the bare minimum)
diff --git a/examples/bug_nonstatic_exception/Makefile b/examples/bug_nonstatic_exception/Makefile
new file mode 100644
index 0000000..f96c1fb
--- /dev/null
+++ b/examples/bug_nonstatic_exception/Makefile
@@ -0,0 +1,82 @@
+SHELL = /bin/sh
+
+# verify that JAVA_HOME is set to something (should be the bare minimum)
+ifndef JAVA_HOME
+ $(error ERROR: Variable JAVA_HOME is not set!)
+endif
+
+
+#
+# C++ compiler options
+#
+CC = g++
+CFLAGS = -g
+INCLUDES = -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux
+LIBS = -ljvm -L$(JAVA_HOME)/lib/server/
+
+#
+# Java compiler option
+#
+JCC = $(JAVA_HOME)/bin/javac
+JFLAGS =
+
+#
+# GIWS options
+#
+GIWS = ../../giws
+GFLAGS = -p --throws-exception-on-error -g
+
+#
+# GIWS PROJECT INFORMATION
+#
+PACKAGE_NAME = bug_nonstatic_exception
+OBJECT_NAME = MyThrowingObject
+BINARY = main
+
+GIWS_CPP_FILE = main.cpp
+GIWS_DESC_FILE = $(OBJECT_NAME).giws.xml
+GIWS_OUT_FILES = $(addprefix $(PACKAGE_NAME),.cpp .hxx)
+
+
+# in case of --throws-exception-on-error, add 2 files to the list
+# of the ones generated by GIWS.
+ifneq (,$(findstring --throws-exception-on-error,$(GFLAGS)))
+ GIWS_OUT_FILES += GiwsException.cpp GiwsException.hxx
+endif
+
+
+#########################################################################
+#########################################################################
+#########################################################################
+
+# look for sources in current folder and in package name
+VPATH = .:$(PACKAGE_NAME)
+
+
+all: $(OBJECT_NAME).class $(BINARY)
+
+
+# build java class file
+$(OBJECT_NAME).class: $(OBJECT_NAME).java
+ $(JCC) $(JFLAGS) $<
+
+
+# build output binary
+$(BINARY): $(GIWS_OUT_FILES)
+ $(CC) $(GIWS_CPP_FILE) $(GIWS_OUT_FILES) $(CFLAGS) $(LIBS) $(INCLUDES) -o $(BINARY)
+ @if test $(MAKELEVEL) -eq 0; then \
+ echo "==========================================================================="; \
+ echo "Dont forget to set library path before running the program:"; \
+ echo "# export LD_LIBRARY_PATH=$(LD_LIBRARY_PATH):$(JAVA_HOME)/lib/server/"; \
+ echo "==========================================================================="; \
+ fi
+
+
+# run giws to generate cpp code
+$(GIWS_OUT_FILES):
+ $(GIWS) -f $(GIWS_DESC_FILE) $(GFLAGS)
+
+
+# cleanup giws generated files (cpp/hxx, bin) and .class
+clean:
+ rm -f $(GIWS_OUT_FILES) $(PACKAGE_NAME)/$(OBJECT_NAME).class $(BINARY)
diff --git a/examples/bug_nonstatic_exception/MyThrowingObject.giws.xml b/examples/bug_nonstatic_exception/MyThrowingObject.giws.xml
new file mode 100644
index 0000000..6d7f518
--- /dev/null
+++ b/examples/bug_nonstatic_exception/MyThrowingObject.giws.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/examples/bug_nonstatic_exception/README b/examples/bug_nonstatic_exception/README
new file mode 100644
index 0000000..f3e0e71
--- /dev/null
+++ b/examples/bug_nonstatic_exception/README
@@ -0,0 +1,16 @@
+Regression test for the missing exception check on non-static methods.
+
+Before giws 3.1.0 the generated code only checked for a pending Java
+exception after calls to static methods. With
+--throws-exception-on-error, an exception thrown by a non-static Java
+method was never converted into a GiwsException::JniCallMethodException.
+
+main returns 0 only if the exception is caught in C++.
+
+Defines where is the JDK
+# export JAVA_HOME=/path/to/java/
+
+Build and run
+# make
+# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JAVA_HOME/lib/server/
+# ./main
diff --git a/examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java b/examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java
new file mode 100644
index 0000000..b649cd9
--- /dev/null
+++ b/examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java
@@ -0,0 +1,8 @@
+package bug_nonstatic_exception;
+
+public class MyThrowingObject {
+
+ public void alwaysThrows() {
+ throw new IllegalStateException("thrown from a non-static method");
+ }
+}
diff --git a/examples/bug_nonstatic_exception/main.cpp b/examples/bug_nonstatic_exception/main.cpp
new file mode 100644
index 0000000..2702253
--- /dev/null
+++ b/examples/bug_nonstatic_exception/main.cpp
@@ -0,0 +1,52 @@
+/*
+Regression test: before giws 3.1.0, the exception check after the Java
+call was only generated for static methods. The Java exception thrown
+by this non-static method was never converted into a C++ exception, so
+the try/catch below did not catch anything and the program fell through
+to the failure branch.
+*/
+
+#include
+#include
+#include "bug_nonstatic_exception.hxx"
+#include "GiwsException.hxx"
+
+JavaVM* create_vm() {
+ JavaVM* jvm;
+ JNIEnv* env;
+ JavaVMInitArgs args;
+ JavaVMOption options[2];
+
+ args.version = JNI_VERSION_1_4;
+
+ args.nOptions = 2;
+ options[0].optionString = const_cast("-Djava.class.path=.");
+ options[1].optionString = const_cast("-Xcheck:jni");
+ args.options = options;
+ args.ignoreUnrecognized = JNI_FALSE;
+
+ JNI_CreateJavaVM(&jvm, (void **)&env, &args);
+ return jvm;
+}
+
+using namespace bug_nonstatic_exception;
+using namespace std;
+
+int main(){
+ JavaVM* jvm = create_vm();
+ MyThrowingObject *obj = new MyThrowingObject(jvm);
+
+ try {
+ obj->alwaysThrows();
+ } catch (GiwsException::JniException & e) {
+ cout << "Exception caught from a non-static method: "
+ << e.getJavaDescription() << endl;
+ cout << "Exception name: " << e.getJavaExceptionName() << endl;
+ delete obj;
+ return 0;
+ }
+
+ cerr << "FAILED: the Java exception was not converted into a C++ exception" << endl;
+ delete obj;
+ return 1;
+}
diff --git a/pyproject.toml b/pyproject.toml
index 5a97319..9f74db4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,3 +31,6 @@ script-files = ["giws"]
[tool.setuptools.dynamic]
version = { attr = "configGiws.__version__" }
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
diff --git a/tests/test_codegen.py b/tests/test_codegen.py
new file mode 100644
index 0000000..89b5639
--- /dev/null
+++ b/tests/test_codegen.py
@@ -0,0 +1,123 @@
+# Copyright or Copr. INRIA/Scilab - Sylvestre LEDRU
+#
+# This software is a computer program whose purpose is to generate C++ wrapper
+# for Java objects/methods.
+#
+# This software is governed by the CeCILL license under French law. See the
+# file COPYING for details.
+
+"""Tests for the code generator.
+
+Each case runs giws on an example XML declaration (with the same flags
+as the example's Makefile) and checks the generated C++. No JDK needed.
+"""
+
+import re
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parent.parent
+EXAMPLES = ROOT / "examples"
+GIWS = ROOT / "giws"
+
+# (example directory, giws flags) - mirrors GFLAGS in examples/*/Makefile
+CASES = [
+ ("basic_example", ["-p"]),
+ ("bug_disable_return", ["-r"]),
+ ("bug_no_param_int_array", []),
+ ("bug_nonstatic_exception", ["-p", "--throws-exception-on-error", "-g"]),
+ ("bug_string_array_len", ["-p"]),
+ ("bytebuffer", ["-p", "--throws-exception-on-error", "-g"]),
+ ("example1", ["-p"]),
+ ("example2", ["-p", "--disable-return-size-array"]),
+ ("example3", ["-p", "--throws-exception-on-error", "-g"]),
+ ("example4", ["-p"]),
+ ("example5", ["-p"]),
+ ("inherit", ["-p"]),
+]
+
+
+def run_giws(args, cwd):
+ return subprocess.run(
+ [sys.executable, str(GIWS), *args],
+ cwd=cwd,
+ capture_output=True,
+ text=True,
+ )
+
+
+def generate(example, flags, destination):
+ """Run giws on an example and return the generated files by name."""
+ xml = next((EXAMPLES / example).glob("*.giws.xml"))
+ shutil.copy(xml, destination / xml.name)
+ result = run_giws(["-f", xml.name, *flags], cwd=destination)
+ assert result.returncode == 0, result.stderr
+ generated = {
+ p.name: p.read_text()
+ for p in destination.iterdir()
+ if p.suffix in (".cpp", ".hxx")
+ }
+ assert generated, f"giws produced no output for {example}"
+ return generated
+
+
+@pytest.mark.parametrize("example,flags", CASES, ids=[c[0] for c in CASES])
+def test_examples_generate(example, flags, tmp_path):
+ generated = generate(example, flags, tmp_path)
+ for name, content in generated.items():
+ assert content.strip(), f"{example}/{name} is empty"
+
+
+def test_nonstatic_method_checks_exceptions(tmp_path):
+ """A Java exception thrown by a non-static method must be detected.
+
+ The check used to be generated for static methods only.
+ """
+ generated = generate(
+ "bug_nonstatic_exception",
+ ["-p", "--throws-exception-on-error", "-g"],
+ tmp_path,
+ )
+ body = generated["bug_nonstatic_exception.cpp"]
+ method = body.split("void MyThrowingObject::alwaysThrows")[1]
+ assert "ExceptionCheck()" in method
+ assert "JniCallMethodException" in method
+
+
+def test_exception_class_checks_after_each_call(tmp_path):
+ """GiwsException helpers must not call JNI functions while an
+ exception raised by CallObjectMethod is pending (-Xcheck:jni)."""
+ generated = generate(
+ "example3", ["-p", "--throws-exception-on-error", "-g"], tmp_path
+ )
+ body = generated["GiwsException.cpp"]
+ calls = list(re.finditer(r"CallObjectMethod\(", body))
+ assert calls, "no CallObjectMethod in GiwsException.cpp"
+ for call in calls:
+ following = body[call.end() : call.end() + 400]
+ assert "ExceptionCheck()" in following, (
+ "CallObjectMethod without a following ExceptionCheck near "
+ f"offset {call.start()}"
+ )
+
+
+def test_version_option():
+ result = run_giws(["--version"], cwd=ROOT)
+ assert result.returncode == 0
+ assert "GIWS " in result.stdout
+
+
+def test_no_arguments_is_an_error():
+ result = run_giws([], cwd=ROOT)
+ assert result.returncode == 2
+ assert "description file" in result.stderr
+
+
+def test_missing_description_file_is_an_error(tmp_path):
+ result = run_giws(["-f", "doesnotexist.xml"], cwd=tmp_path)
+ assert result.returncode == 2
+ assert "doesnotexist.xml" in result.stderr