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 (>= 1.5, because of varargs and enums).

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. Call the module NLopt instead of nlopt for SWIGJAVA, because in
Java, this is the name of the globals class (not the package, which is
set separately on the SWIG command line), so it should be in CamelCase.
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, and because it should be in CamelCase. 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.) Rename nlopt::opt::get_initial_step_ to the
lowerCamelCase getInitialStep instead of get_initial_step. 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.) Update the path handling for the
Python and Guile bindings accordingly. 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. For the
Guile binding, set and unset CMAKE_SWIG_FLAGS instead of using
set_source_files_properties on nlopt.i, because nlopt.i is shared for
all bindings, so setting the property on the source file does not help
preventing breaking other target language bindings (such as Java).

src/swig/glob_java.cmake: New helper script whose main 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. In addition, it also does a
replacement in DoubleVector.java, changing double[] initialElements to
double... initialElements (introduced in Java 1.5), which needs to
happen at the same stage of the build, so that initial elements can be
passed directly to new DoubleVector without extra new double[]{
boilerplate.

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.) Update
the PYINSTALLCHECK_ENVIRONMENT settings for the src/swig/CMakeLists.txt
changes, so that the Python tests keep passing. Update the
GUILE_LOAD_PATH settings for the src/swig/CMakeLists.txt changes, so
that the Guile tests keep passing.

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.

doc/docs/NLopt_Java_Reference.md: New file, based on
NLopt_Python_Reference.md and NLopt_Guile_Reference.md, adapted to the
Java syntax.

doc/docs/NLopt_Tutorial.md: Add example in Java.

doc/docs/index.md: Mention Java, linking to NLopt_Java_Reference.md.
  • Loading branch information
kkofler committed Dec 6, 2024
1 parent 8c6e52c commit 819ee46
Show file tree
Hide file tree
Showing 10 changed files with 880 additions and 10 deletions.
22 changes: 21 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 @@ -340,6 +341,25 @@ 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)
# FindJNI.cmake in CMake versions prior to 3.24 does not export any targets
if(JNI_FOUND AND NOT TARGET JNI::JNI)
add_library(JNI::JNI IMPORTED INTERFACE)
set_property(TARGET JNI::JNI PROPERTY INTERFACE_INCLUDE_DIRECTORIES
${JAVA_INCLUDE_PATH})
if(NOT JNI_INCLUDE_PATH2_OPTIONAL AND JAVA_INCLUDE_PATH2)
set_property(TARGET JNI::JNI APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES
${JAVA_INCLUDE_PATH2})
endif()
endif()
find_package (Java 1.5)
endif ()

if (NLOPT_SWIG)
find_package (SWIG 3)
if (SWIG_FOUND)
Expand Down
423 changes: 423 additions & 0 deletions doc/docs/NLopt_Java_Reference.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions doc/docs/NLopt_Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,58 @@ On error conditions, the NLopt functions throw [exceptions](http://www.gnu.org/s

The heavy use of side-effects here is a bit unnatural in Scheme, but is used in order to closely map to the C++ interface. (Notice that `nlopt::` C++ functions map to `nlopt-` Guile functions, and `nlopt::opt::` methods map to `nlopt-opt-` functions that take the `opt` object as the first argument.) Of course, you are free to wrap your own Scheme-like functional interface around this if you wish.

Example in Java
---------------

In Java (1.8 or later), the equivalent of the example above would be:

```java
import nlopt.*;

public class t_java {
private static double myfunc(double[] x, double[] grad) {
if (grad != null) {
grad[0] = 0.0;
grad[1] = 0.5 / Math.sqrt(x[1]);
}
return Math.sqrt(x[1]);
}

private static double myconstraint(double[] x, double[] grad, double a,
double b) {
if (grad != null) {
grad[0] = 3 * a * (a*x[0] + b) * (a*x[0] + b);
grad[1] = -1.0;
}
return ((a*x[0] + b) * (a*x[0] + b) * (a*x[0] + b) - x[1]);
}

public static void main(String[] args) {
System.loadLibrary("nloptjni");
Opt opt = new Opt(Algorithm.LD_MMA, 2);
opt.setLowerBounds(new DoubleVector(Double.NEGATIVE_INFINITY, 0.));
opt.setMinObjective(t_java::myfunc);
opt.addInequalityConstraint((x, grad) -> myconstraint(x, grad, 2, 0), 1e-8);
opt.addInequalityConstraint((x, grad) -> myconstraint(x, grad, -1, 1),
1e-8);
opt.setXtolRel(1e-4);
DoubleVector x = opt.optimize(new DoubleVector(1.234, 5.678));
double minf = opt.lastOptimumValue();
System.out.println("optimum at " + x);
System.out.println("minimum value: " + minf);
System.out.println("result code: " + opt.lastOptimizeResult());
}
}
```

Note that the objective/constraint functions take two arguments, `x` and `grad`, and return a number. `x` is a vector whose length is the dimension of the problem; grad is either `null` if it is not needed, or a `DoubleVector` that must be modified *in-place* to the gradient of the function.

Also note that the above example uses lambdas, both in the explicit `(x, grad) -> ` notation and using the `::` operator, so it will compile as is only with Java 1.8 or later. The Java binding supports Java 1.5 or later, but if you wish to support versions 1.5 to 1.7, you cannot use lambdas. Instead, for Java prior to 1.8, you would have to explicitly declare the anonymous classes implementing the interfaces, leading to less readable code.

On error conditions, the NLopt functions throw Java runtime exceptions (unchecked exceptions) that can be caught by your Java code if you wish.

Note that the class and method names are renamed to camel case as usual in Java: upper camel case for classes, lower camel case for methods. The exception classes additionally have `Exception` appended to their names. The `nlopt` C++ namespace maps to the `nlopt` Java package, global `nlopt::` C++ functions map to static methods of the `nlopt.NLopt` class, methods of classes (e.g., `nlopt::opt`) map to the methods of the corresponding Java class (e.g., `nlopt.Opt`), and `std::vector<double>` maps to `nlopt.DoubleVector`.

Example in Fortran
------------------

Expand Down
2 changes: 1 addition & 1 deletion doc/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ NLopt

**NLopt** is a free/open-source library for **nonlinear optimization**, providing a common interface for a number of different free optimization routines available online as well as original implementations of various other algorithms. Its features include:

- Callable from [C](NLopt_Reference.md), [C++](NLopt_C-plus-plus_Reference.md), [Fortran](NLopt_Fortran_Reference.md), [Matlab or GNU Octave](NLopt_Matlab_Reference.md), [Python](NLopt_Python_Reference.md), [GNU Guile](NLopt_Guile_Reference.md), [Julia](https://github.com/stevengj/NLopt.jl), [GNU R](NLopt_R_Reference.md), [Lua](https://github.com/rochus-keller/LuaNLopt), [OCaml](https://bitbucket.org/mkur/nlopt-ocaml), [Rust](https://github.com/adwhit/rust-nlopt) and [Crystal](https://github.com/konovod/nlopt.cr).
- Callable from [C](NLopt_Reference.md), [C++](NLopt_C-plus-plus_Reference.md), [Fortran](NLopt_Fortran_Reference.md), [Matlab or GNU Octave](NLopt_Matlab_Reference.md), [Python](NLopt_Python_Reference.md), [GNU Guile](NLopt_Guile_Reference.md), [Java](NLopt_Java_Reference.md), [Julia](https://github.com/stevengj/NLopt.jl), [GNU R](NLopt_R_Reference.md), [Lua](https://github.com/rochus-keller/LuaNLopt), [OCaml](https://bitbucket.org/mkur/nlopt-ocaml), [Rust](https://github.com/adwhit/rust-nlopt) and [Crystal](https://github.com/konovod/nlopt.cr).
- A common interface for [many different algorithms](NLopt_Algorithms.md)—try a different algorithm just by changing one parameter.
- Support for large-scale optimization (some algorithms scalable to millions of parameters and thousands of constraints).
- Both global and local optimization algorithms.
Expand Down
63 changes: 58 additions & 5 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 All @@ -24,7 +26,9 @@ if (Python_NumPy_FOUND)
set (SWIG_MODULE_nlopt_python_EXTRA_DEPS nlopt-python.i numpy.i generate-cpp)

# swig_add_module is deprecated
swig_add_library (nlopt_python LANGUAGE python SOURCES nlopt.i)
swig_add_library (nlopt_python LANGUAGE python SOURCES nlopt.i
OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/python
OUTFILE_DIR ${CMAKE_CURRENT_BINARY_DIR})

target_link_libraries (nlopt_python ${nlopt_lib})
target_link_libraries (nlopt_python Python::NumPy)
Expand All @@ -33,7 +37,7 @@ if (Python_NumPy_FOUND)
set_target_properties (nlopt_python PROPERTIES OUTPUT_NAME nlopt)
set_target_properties (nlopt_python PROPERTIES COMPILE_FLAGS "${SWIG_COMPILE_FLAGS}")

install (FILES ${CMAKE_CURRENT_BINARY_DIR}/nlopt.py DESTINATION ${INSTALL_PYTHON_DIR})
install (FILES ${CMAKE_CURRENT_BINARY_DIR}/python/nlopt.py DESTINATION ${INSTALL_PYTHON_DIR})
install (TARGETS nlopt_python DESTINATION ${INSTALL_PYTHON_DIR})

configure_file (METADATA.in METADATA @ONLY)
Expand All @@ -44,11 +48,15 @@ endif ()

if (GUILE_FOUND)

set_source_files_properties (nlopt.i PROPERTIES SWIG_FLAGS "-scmstub")
set (SWIG_MODULE_nlopt_guile_EXTRA_DEPS nlopt-guile.i generate-cpp)
set (CMAKE_SWIG_FLAGS -scmstub)

# swig_add_module is deprecated
swig_add_library (nlopt_guile LANGUAGE guile SOURCES nlopt.i)
swig_add_library (nlopt_guile LANGUAGE guile SOURCES nlopt.i
OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/guile
OUTFILE_DIR ${CMAKE_CURRENT_BINARY_DIR})

set (CMAKE_SWIG_FLAGS)

target_include_directories (nlopt_guile PRIVATE ${GUILE_INCLUDE_DIRS})

Expand All @@ -58,9 +66,54 @@ if (GUILE_FOUND)

file (RELATIVE_PATH _REL_GUILE_SITE_PATH ${GUILE_ROOT_DIR} ${GUILE_SITE_DIR})
set (GUILE_SITE_PATH ${_REL_GUILE_SITE_PATH})
install (FILES ${CMAKE_CURRENT_BINARY_DIR}/nlopt.scm DESTINATION ${GUILE_SITE_PATH})
install (FILES ${CMAKE_CURRENT_BINARY_DIR}/guile/nlopt.scm DESTINATION ${GUILE_SITE_PATH})

file (RELATIVE_PATH _REL_GUILE_EXTENSION_PATH ${GUILE_ROOT_DIR} ${GUILE_EXTENSION_DIR})
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)
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
set_target_properties (nlopt_java PROPERTIES
COMPILE_OPTIONS "-fno-strict-aliasing")
endif ()

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 "${swig_generated_file_fullname}"
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)
install_jar (nlopt_jar ${CMAKE_INSTALL_DATADIR}/java)
endif ()
13 changes: 13 additions & 0 deletions src/swig/glob_java.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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})

# SWIG hardcodes non-vararg initial elements for std::vector wrappers,
# probably to support Java versions older than 1.5. We do not really care
# about supporting a Java that old, so fix the generated code.
file(READ ${BINARY_DIR}/java/nlopt/DoubleVector.java FILE_CONTENTS)
string(REPLACE "double[] initialElements" "double... initialElements" FILE_CONTENTS "${FILE_CONTENTS}")
file(WRITE ${BINARY_DIR}/java/nlopt/DoubleVector.java "${FILE_CONTENTS}")
Loading

0 comments on commit 819ee46

Please sign in to comment.