Skip to content

Commit

Permalink
Add Java binding using the existing SWIG infrastructure
Browse files Browse the repository at this point in the history
CMakeLists.txt: Add NLOPT_JAVA option. If set, check for C++, JNI, and
Java.

src/swig/nlopt-java.i: New SWIG input file (not standalone, but to be
included into nlopt.i). Contains the Java-specific declarations.

src/swig/nlopt.i: Add syntax highlighting declaration for KatePart-based
editors. Instantiate vector<double> as doublevector instead of
nlopt_doublevector for SWIGJAVA, because everything is in an nlopt
package in Java, so the nlopt_ part is redundant. Ignore
nlopt_get_initial_step because at least for Java, SWIG wants to generate
a binding for it, using SWIGTYPE_p_* types that cannot be practically
used. (It is in-place just like nlopt::opt::get_initial_step, which is
already ignored.) For SWIGJAVA, %include nlopt-java.i.

src/swig/CMakeLists.txt: Set UseSWIG_MODULE_VERSION to 2 so that UseSWIG
cleans up old generated source files before running SWIG, useful for
Java. (In particular, it prevents obsolete .java files from old .i file
versions, which might not even compile anymore, from being included in
the Java compilation and the JAR.) If JNI, Java, and SWIG were found,
generate the Java binding with SWIG, and compile the JNI library with
the C++ compiler and the Java JAR with the Java compiler.

src/swig/glob_java.cmake: New helper script whose only purpose is to
invoke the CMake file(GLOB ...) command at the correct stage of the
build process, after the SWIG run, not when CMake is run on the main
CMakeLists.txt, because the latter happens before anything at all is
built. The script is invoked through cmake -P by an add_custom_command
in CMakeLists.txt, whose dependencies order it to the correct spot of
the build. This is the only portable way to automatically determine
which *.java files SWIG has generated from the *.i files. The result
is written to java_sources.txt, which is dynamically read by the add_jar
command thanks to the @ indirection.

test/t_java.java: New test. Java port of t_tutorial.cxx/t_python.py.

test/CMakeLists.txt: If JNI, Java >= 1.8, and SWIG were found, run the
t_java test program with the algorithms 23 (MMA), 24 (COBYLA), 30
(augmented Lagrangian), and 39 (SLSQP). All 4 tests pass. (Java < 1.8
should be supported by the binding itself, but not by the test.)

This code is mostly unrelated to the old unmaintained nlopt4j binding
(https://github.com/dibyendumajumdar/nlopt4j), which used handwritten
JNI code. Instead, it reuses the existing SWIG binding infrastructure,
adding Java as a supported language to that. Only the code in the
func_java and mfunc_java wrappers is loosely inspired by the nlopt4j
code. The rest of the code in this binding relies mostly on SWIG
features and uses very little handwritten JNI code, so nlopt4j was not
useful as a reference for it. This binding is also
backwards-incompatible with nlopt4j due to differing naming conventions.

Note that this binding maps the C++ class and method names identically
to Java. As a result, it does not use idiomatic Java case conventions,
but uses snake_case for both class and method names instead of the usual
UpperCamelCase for class names and lowerCamelCase for method names in
Java. The C++ namespace nlopt is mapped to the Java package nlopt.
  • Loading branch information
kkofler committed Nov 29, 2024
1 parent 11cff2c commit 22c7bf4
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 1 deletion.
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ option (NLOPT_PYTHON "build python bindings" ON)
option (NLOPT_OCTAVE "build octave bindings" ON)
option (NLOPT_MATLAB "build matlab bindings" OFF)
option (NLOPT_GUILE "build guile bindings" ON)
option (NLOPT_JAVA "build java bindings" ON)
option (NLOPT_SWIG "use SWIG to build bindings" ON)
option (NLOPT_LUKSAN "enable LGPL Luksan solvers" ON)
option (NLOPT_TESTS "build unit tests" OFF)
Expand Down Expand Up @@ -143,7 +144,7 @@ if (WITH_THREADLOCAL AND NOT DEFINED THREADLOCAL)
endif ()


if (NLOPT_CXX OR NLOPT_PYTHON OR NLOPT_GUILE OR NLOPT_OCTAVE)
if (NLOPT_CXX OR NLOPT_PYTHON OR NLOPT_GUILE OR NLOPT_OCTAVE OR NLOPT_JAVA)
check_cxx_symbol_exists (__cplusplus ciso646 SYSTEM_HAS_CXX)
if (SYSTEM_HAS_CXX)
set (CMAKE_CXX_STANDARD 11) # set the standard to C++11 but do not require it
Expand Down Expand Up @@ -344,6 +345,15 @@ if (NLOPT_GUILE)
find_package (Guile)
endif ()

if (NLOPT_JAVA)
# we do not really need any component, only the main JNI target, but if the
# list of components is left empty, FindJNI defaults to "JVM AWT", and we
# specifically do not want to check for the AWT library that is not available
# on headless installations
find_package (JNI COMPONENTS JVM)
find_package (Java)
endif ()

if (NLOPT_SWIG)
find_package (SWIG 3)
if (SWIG_FOUND)
Expand Down
41 changes: 41 additions & 0 deletions src/swig/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
if (POLICY CMP0078)
cmake_policy(SET CMP0078 NEW)
endif ()
# clean up old generated source files before running SWIG, useful for Java
set(UseSWIG_MODULE_VERSION 2)
include (UseSWIG)

# allows one set C++ flags for swig wrappers
Expand Down Expand Up @@ -71,3 +73,42 @@ if (GUILE_FOUND)
set (GUILE_EXTENSION_PATH ${_REL_GUILE_EXTENSION_PATH})
install (TARGETS nlopt_guile LIBRARY DESTINATION ${GUILE_EXTENSION_PATH})
endif ()


if (JNI_FOUND AND Java_FOUND AND SWIG_FOUND)

include (UseJava)

set (SWIG_MODULE_nlopt_java_EXTRA_DEPS nlopt-java.i generate-cpp)
set (CMAKE_SWIG_FLAGS -package nlopt)

# swig_add_module is deprecated
# OUTPUT_DIR is ${CMAKE_CURRENT_BINARY_DIR}/java/ + the -package above (with
# any '.' replaced by '/'). It must also match the GLOB in glob_java.cmake.
swig_add_library (nlopt_java LANGUAGE java SOURCES nlopt.i
OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/java/nlopt
OUTFILE_DIR ${CMAKE_CURRENT_BINARY_DIR})

set (CMAKE_SWIG_FLAGS)

swig_link_libraries (nlopt_java ${nlopt_lib})
target_link_libraries (nlopt_java JNI::JNI)

set_target_properties (nlopt_java PROPERTIES OUTPUT_NAME nloptjni)

install (TARGETS nlopt_java LIBRARY DESTINATION ${NLOPT_INSTALL_LIBDIR})

# unfortunately, SWIG will not tell us which .java files it generated, so we
# have to find out ourselves - this is the only portable way to do so
# (The nlopt*.i dependencies are there to force updating the list of sources
# on any changes to the SWIG interface code, they are not direct inputs.)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/java_sources.txt
COMMAND ${CMAKE_COMMAND}
-DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}
-P ${CMAKE_CURRENT_SOURCE_DIR}/glob_java.cmake
DEPENDS nlopt.i nlopt-exceptions.i nlopt-java.i
nlopt_java_swig_compilation glob_java.cmake)

add_jar (nlopt_jar SOURCES @${CMAKE_CURRENT_BINARY_DIR}/java_sources.txt
OUTPUT_NAME nlopt)
endif ()
6 changes: 6 additions & 0 deletions src/swig/glob_java.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This file(GLOB ...) must run at build (make) time, after the SWIG run. So it
# cannot be invoked directly from CMakeLists.txt, but must be invoked through
# cmake -P at the correct spot of the build, using add_custom_command.
file(GLOB JAVA_SOURCES ${BINARY_DIR}/java/nlopt/*.java)
list(JOIN JAVA_SOURCES "\n" JAVA_SOURCES_LINES)
file(WRITE ${BINARY_DIR}/java_sources.txt ${JAVA_SOURCES_LINES})
220 changes: 220 additions & 0 deletions src/swig/nlopt-java.i
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// -*- C++ -*-
// kate: hl c++

// use proper Java enums
%include "enums.swg"
// use Java code for the constants in the enums instead of calling a C function
%javaconst(1);

// pointer-based API not supported, use version_{major,minor,bugfix} instead
%ignore version;
// pointer-based API not supported, use the other overload instead
%ignore optimize(std::vector<double> &, double &);
// unsupported function APIs, use the ones with nlopt_munge instead
%ignore set_min_objective(func, void *);
%ignore set_min_objective(vfunc, void *);
%ignore set_min_objective(functor_type);
%ignore set_max_objective(func, void *);
%ignore set_max_objective(vfunc, void *);
%ignore set_max_objective(functor_type);
%ignore add_inequality_constraint(func, void *);
%ignore add_inequality_constraint(func, void *, double);
%ignore add_inequality_constraint(vfunc, void *);
%ignore add_inequality_constraint(vfunc, void *, double);
%ignore add_inequality_mconstraint(mfunc, void *, const std::vector<double> &);
%ignore add_equality_constraint(func, void *);
%ignore add_equality_constraint(func, void *, double);
%ignore add_equality_constraint(vfunc, void *);
%ignore add_equality_constraint(vfunc, void *, double);
%ignore add_equality_mconstraint(mfunc, void *, const std::vector<double> &);

// Munge function types
%extend nlopt::opt {
%proxycode {
public static interface func {
public double apply(double[] x, double[] gradient);
}

public static interface mfunc {
public double[] apply(double[] x, double[] gradient);
}
}
}

%{
struct jfunc {
JNIEnv *jenv;
jobject func;
jmethodID method;
};

static void *free_jfunc(void *p) {
((jfunc *) p)->jenv->DeleteGlobalRef(((jfunc *) p)->func);
delete (jfunc *) p;
return (void *) 0;
}

static void *dup_jfunc(void *p) {
jfunc *q = new jfunc;
q->jenv = ((jfunc *) p)->jenv;
q->func = q->jenv->NewGlobalRef(((jfunc *) p)->func);
q->method = ((jfunc *) p)->method;
return (void *) q;
}

static double func_java(unsigned n, const double *x, double *grad, void *f)
{
JNIEnv *jenv = ((jfunc *) f)->jenv;
jobject func = ((jfunc *) f)->func;
jmethodID method = ((jfunc *) f)->method;

jdoubleArray jx = jenv->NewDoubleArray(n);
if (!jx || jenv->ExceptionCheck()) {
throw nlopt::forced_stop();
}
jenv->SetDoubleArrayRegion(jx, 0, n, x);
jdoubleArray jgrad = (jdoubleArray) 0;
if (grad) {
jgrad = jenv->NewDoubleArray(n);
if (!jgrad || jenv->ExceptionCheck()) {
jenv->DeleteLocalRef(jx);
throw nlopt::forced_stop();
}
jenv->SetDoubleArrayRegion(jgrad, 0, n, grad);
}

jdouble res = jenv->CallDoubleMethod(func, method, jx, jgrad);
jenv->DeleteLocalRef(jx);

if (jenv->ExceptionCheck()) {
if (jgrad) {
jenv->DeleteLocalRef(jgrad);
}
throw nlopt::forced_stop();
}

if (grad) {
jenv->GetDoubleArrayRegion(jgrad, 0, n, grad);
jenv->DeleteLocalRef(jgrad);
}

return res;
}

static void mfunc_java(unsigned m, double *result,
unsigned n, const double *x, double *grad, void *f)
{
JNIEnv *jenv = ((jfunc *) f)->jenv;
jobject func = ((jfunc *) f)->func;
jmethodID method = ((jfunc *) f)->method;

jdoubleArray jx = jenv->NewDoubleArray(n);
if (!jx || jenv->ExceptionCheck()) {
throw nlopt::forced_stop();
}
jenv->SetDoubleArrayRegion(jx, 0, n, x);
jdoubleArray jgrad = (jdoubleArray) 0;
if (grad) {
jgrad = jenv->NewDoubleArray(m * n);
if (!jgrad || jenv->ExceptionCheck()) {
jenv->DeleteLocalRef(jx);
throw nlopt::forced_stop();
}
jenv->SetDoubleArrayRegion(jgrad, 0, m * n, grad);
}

jdoubleArray res = (jdoubleArray) jenv->CallObjectMethod(func, method, jx, jgrad);
jenv->DeleteLocalRef(jx);

if (!res || jenv->ExceptionCheck()) {
if (jgrad) {
jenv->DeleteLocalRef(jgrad);
}
if (res) {
jenv->DeleteLocalRef(res);
}
throw nlopt::forced_stop();
}

jenv->GetDoubleArrayRegion(res, 0, m, result);
jenv->DeleteLocalRef(res);

if (grad) {
jenv->GetDoubleArrayRegion(jgrad, 0, m * n, grad);
jenv->DeleteLocalRef(jgrad);
}
}
%}

%typemap(jni)(nlopt::func f, void *f_data, nlopt_munge md, nlopt_munge mc) "jobject"
%typemap(jtype)(nlopt::func f, void *f_data, nlopt_munge md, nlopt_munge mc) "java.lang.Object"
%typemap(jstype)(nlopt::func f, void *f_data, nlopt_munge md, nlopt_munge mc) "func"
%typemap(in)(nlopt::func f, void *f_data, nlopt_munge md, nlopt_munge mc) {
$1 = func_java;
jfunc jf = {jenv, $input, jenv->GetMethodID(jenv->FindClass("nlopt/opt$func"), "apply", "([D[D)D")};
$2 = dup_jfunc((void *) &jf);
$3 = free_jfunc;
$4 = dup_jfunc;
}
%typemap(javain)(nlopt::func f, void *f_data, nlopt_munge md, nlopt_munge mc) "$javainput"

%typemap(jni)(nlopt::mfunc mf, void *f_data, nlopt_munge md, nlopt_munge mc) "jobject"
%typemap(jtype)(nlopt::mfunc mf, void *f_data, nlopt_munge md, nlopt_munge mc) "java.lang.Object"
%typemap(jstype)(nlopt::mfunc mf, void *f_data, nlopt_munge md, nlopt_munge mc) "mfunc"
%typemap(in)(nlopt::mfunc mf, void *f_data, nlopt_munge md, nlopt_munge mc) {
$1 = mfunc_java;
jfunc jf = {jenv, $input, jenv->GetMethodID(jenv->FindClass("nlopt/opt$mfunc"), "apply", "([D[D)[D")};
$2 = dup_jfunc((void *) &jf);
$3 = free_jfunc;
$4 = dup_jfunc;
}
%typemap(javain)(nlopt::mfunc mf, void *f_data, nlopt_munge md, nlopt_munge mc) "$javainput"

// Make exception classes Java-compliant
%typemap(javabase) nlopt::forced_stop "java.lang.RuntimeException"
%typemap(javabody) nlopt::forced_stop ""
%typemap(javadestruct) nlopt::forced_stop ""
%typemap(javafinalize) nlopt::forced_stop ""
%ignore nlopt::forced_stop::forced_stop;
%extend nlopt::forced_stop {
%proxycode {
public forced_stop(String message) {
super(message);
}
}
}
%typemap(javabase) nlopt::roundoff_limited "java.lang.RuntimeException"
%typemap(javabody) nlopt::roundoff_limited ""
%typemap(javadestruct) nlopt::roundoff_limited ""
%typemap(javafinalize) nlopt::roundoff_limited ""
%ignore nlopt::roundoff_limited::roundoff_limited;
%extend nlopt::roundoff_limited {
%proxycode {
public roundoff_limited(String message) {
super(message);
}
}
}

// Map exceptions
%typemap(throws) std::bad_alloc %{
SWIG_JavaThrowException(jenv, SWIG_JavaOutOfMemoryError, $1.what());
return $null;
%}

%typemap(throws) nlopt::forced_stop %{
jenv->ExceptionClear();
jclass excep = jenv->FindClass("nlopt/forced_stop");
if (excep)
jenv->ThrowNew(excep, $1.what());
return $null;
%}

%typemap(throws) nlopt::roundoff_limited %{
jenv->ExceptionClear();
jclass excep = jenv->FindClass("nlopt/roundoff_limited");
if (excep)
jenv->ThrowNew(excep, $1.what());
return $null;
%}

10 changes: 10 additions & 0 deletions src/swig/nlopt.i
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// -*- C++ -*-
// kate: hl c++

%define DOCSTRING
"NLopt is a multi-language library for nonlinear optimization (local or
Expand All @@ -18,13 +19,18 @@ can be found at the NLopt web page: http://ab-initio.mit.edu/nlopt"
%include "std_except.i"
%include "std_vector.i"
namespace std {
#ifdef SWIGJAVA
%template(doublevector) vector<double>;
#else
%template(nlopt_doublevector) vector<double>;
#endif
};

%ignore nlopt::opt::myfunc_data;
%ignore nlopt::opt::operator=;

// dont use the in-place version of get_initial_step
%ignore nlopt_get_initial_step;
%ignore nlopt::opt::get_initial_step;
%rename(get_initial_step) nlopt::opt::get_initial_step_;

Expand Down Expand Up @@ -53,4 +59,8 @@ namespace std {
%include "nlopt-python.i"
#endif

#ifdef SWIGJAVA
%include "nlopt-java.i"
#endif

%include "nlopt.hpp"
18 changes: 18 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ if (Python_FOUND AND NUMPY_FOUND AND (SWIG_FOUND OR (EXISTS ${PROJECT_SOURCE_DIR
set_tests_properties (test_memoize PROPERTIES ENVIRONMENT "${PYINSTALLCHECK_ENVIRONMENT}")
endif ()

# The test uses lambdas and the :: operator, which were introduced in Java 1.8.
# The binding itself should also compile with more ancient Java versions that
# have already reached their end of life, but it is not worth uglifying the test
# code for them, because people will then cargo-cult the legacy boilerplate.
if (JNI_FOUND AND Java_FOUND AND SWIG_FOUND AND NOT Java_VERSION VERSION_LESS 1.8)
include (UseJava)
add_jar (t_java SOURCES t_java.java INCLUDE_JARS nlopt_jar ENTRY_POINT t_java)
get_property (t_java_jar TARGET t_java PROPERTY JAR_FILE)
get_property (nlopt_jar_jar TARGET nlopt_jar PROPERTY JAR_FILE)
set (nlopt_java_dir $<TARGET_FILE_DIR:nlopt_java>)
foreach (algo_index 23 24 30 39)
add_test (NAME test_java${algo_index}
COMMAND ${Java_JAVA_EXECUTABLE} -cp ${t_java_jar}:${nlopt_jar_jar}
-Djava.library.path=${nlopt_java_dir} t_java
${algo_index})
endforeach()
endif ()

if (OCTAVE_FOUND)
add_test (NAME test_octave COMMAND ${OCTAVE_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/t_octave.m ${PROJECT_SOURCE_DIR}/src/octave ${PROJECT_BINARY_DIR}/src/octave)
endif ()
Expand Down
Loading

0 comments on commit 22c7bf4

Please sign in to comment.