From 424ac55c38ca65da2f96eb8fe4e8e4d6b73a86e1 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Aug 2026 22:34:04 +0200 Subject: [PATCH 1/7] GiwsException: check for pending exceptions after each CallObjectMethod The retrieve helpers of the generated exception class called GetStringUTFChars/GetArrayLength right after CallObjectMethod without an ExceptionCheck in between, which -Xcheck:jni flags as 'JNI call made without checking exceptions'. If retrieving the message itself throws, we now clear the secondary exception, release the local refs and fall back to an empty string. example3 runs warning-free. --- CXXException.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) 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 ""; From d184ea41ec5f8710b79477d085afa28ceb4e6767 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Aug 2026 22:35:04 +0200 Subject: [PATCH 2/7] Replace the abstractMethod helper with NotImplementedError The stock idiom cannot break the way the helper did in 89a38c3, and subclasses failing to override now raise the standard exception with the offending class name. --- datatypes/dataGiws.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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""" From ba2467ab1a1c46efdee3998589bf22ebb0fa4004 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Aug 2026 22:40:40 +0200 Subject: [PATCH 3/7] Complete the 3.1.0 changelog entry with the follow-up fixes --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index eebc337..9b773a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,9 @@ 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) * Modernize the Python code (f-strings, type hints, with-statement) * Add a pre-commit configuration running ruff From 5ba3dce070342ab97518a3e84fc41cc0ea4a86b7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Aug 2026 22:59:28 +0200 Subject: [PATCH 4/7] Add a test suite for the code generator Runs giws on every example XML with the same flags as the example's Makefile and checks the generated C++: generation succeeds, non-static methods get the post-call exception check, and the GiwsException helpers check for pending exceptions after each CallObjectMethod. Plus a few CLI behavior tests. No JDK needed, runs in under a second. --- pyproject.toml | 3 ++ tests/test_codegen.py | 123 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 tests/test_codegen.py 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 From 4d2a7fa63578aecd5911855ec5a4c3e72696d8e9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Aug 2026 22:59:28 +0200 Subject: [PATCH 5/7] Add the bug_nonstatic_exception example Regression test for the missing exception check on non-static methods: main returns 0 only if the IllegalStateException thrown by the non-static Java method is caught as a C++ JniException. Wired into the examples build and the test suite. --- CHANGELOG | 3 + examples/Makefile | 2 +- examples/bug_nonstatic_exception/Makefile | 82 +++++++++++++++++++ .../MyThrowingObject.giws.xml | 12 +++ examples/bug_nonstatic_exception/README | 16 ++++ .../MyThrowingObject.java | 8 ++ examples/bug_nonstatic_exception/main.cpp | 52 ++++++++++++ 7 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 examples/bug_nonstatic_exception/Makefile create mode 100644 examples/bug_nonstatic_exception/MyThrowingObject.giws.xml create mode 100644 examples/bug_nonstatic_exception/README create mode 100644 examples/bug_nonstatic_exception/bug_nonstatic_exception/MyThrowingObject.java create mode 100644 examples/bug_nonstatic_exception/main.cpp diff --git a/CHANGELOG b/CHANGELOG index 9b773a4..04d291c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,9 @@ giws (3.1.0) * 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/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; +} From f2f8d730604cad259dc8d529b6a47fed0339ed3a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 6 Aug 2026 22:59:28 +0200 Subject: [PATCH 6/7] CI: single workflow running ruff, unit tests and the examples build Fold lint.yml into ci.yml so pull requests get ruff (check + format) and the pytest suite alongside the examples build; PRs against any branch now trigger it. --- .github/workflows/ci.yml | 25 +++++++++++++++++++++---- .github/workflows/lint.yml | 15 --------------- 2 files changed, 21 insertions(+), 19 deletions(-) delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f02d203..bd5ee0c 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 @@ -46,7 +60,10 @@ 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: | 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 From 2c53f3577ff0e14067cadd70bf64146b39b4fc18 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 7 Aug 2026 08:47:30 +0200 Subject: [PATCH 7/7] fix the CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd5ee0c..8f7a3de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,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 @@ -69,6 +69,6 @@ jobs: 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