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
102 changes: 102 additions & 0 deletions .github/workflows/jni.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: jni

on:
push:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 1G
CCACHE_KEY_SUFFIX: r1
CONAN_HOME: ${{ github.workspace }}/.conan2
CONAN_KEY_SUFFIX: r1

jobs:
build-test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-24.04, build_profile: ubuntu-24.04-clang-18, host_profile: ubuntu-24.04-clang-18 }
- { os: macos-26, build_profile: macos-26-armv8-clang-14, host_profile: macos-26-armv8-clang-14 }
steps:
- name: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: checkout conan-odr-index
run: git submodule update --init --depth 1 conan-odr-index

- name: ubuntu install ccache
if: runner.os == 'Linux'
run: |
sudo apt install ccache
ccache -V
- name: macos install ccache
if: runner.os == 'macOS'
run: |
brew install ccache
ccache -V

- name: setup java
uses: actions/setup-java@f4f1212c880fdec8162ea9a6493f4495191887b4 # v5
with:
distribution: temurin
java-version: 21

- name: setup python 3.14
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: 3.14
- name: install python dependencies
run: pip install conan

- name: cache conan
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ env.CONAN_HOME }}
key: conan-jni-${{ matrix.host_profile }}-${{ env.CONAN_KEY_SUFFIX }}
restore-keys: |
conan-jni-${{ matrix.host_profile }}-
conan-${{ matrix.host_profile }}-

- name: export conan-odr-index
run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml
- name: conan config
run: conan config install .github/config/conan

- name: cache ccache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ env.CCACHE_DIR }}
key: ccache-jni-${{ matrix.host_profile }}-${{ env.CCACHE_KEY_SUFFIX }}
restore-keys: |
ccache-jni-${{ matrix.host_profile }}-
ccache-${{ matrix.host_profile }}-

- name: conan install
run: >
conan install .
-o '&:with_jni=True'
--profile:host '${{ matrix.host_profile }}'
--profile:build '${{ matrix.build_profile }}'
--build missing

- name: cmake
run: >
cmake -B build -S .
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_CXX_FLAGS="-Werror"
-DODR_JNI=ON
-DODR_CLI=OFF
-DODR_TEST=ON

- name: build
run: cmake --build build --target odr_jni odr_java odr_java_tests --config Release

- name: junit
run: ctest --test-dir build/jni --output-on-failure
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ bytes ─▶ magic/open_strategy ─▶ DecodedFile ─▶ Document ─▶ Eleme
| `src/odr/internal/{csv,json,text,svm}/` | Smaller formats. |
| `cli/src/` | CLI tools: `translate`, `back_translate`, `meta`, `server`. |
| `python/` | Python bindings (`pyodr`, pybind11); see [`python/AGENTS.md`](python/AGENTS.md). |
| `jni/` | JNI bindings (Java package `app.opendocument.core`); see [`jni/AGENTS.md`](jni/AGENTS.md). |
| `tools/pdf/` | Dev tooling (not built): PDF encoding-data generators, see `tools/pdf/README.md`. |
| `test/src/` | GoogleTest suites; data in `test/data` (git submodules). |
| `offline/documentation/MS-*/` | Vendored Microsoft spec text (see [Specs](#specs)). |
Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ option(ODR_WITH_WVWARE "Build with wvWare" "${WITH_WVWARE}")
option(ODR_WITH_LIBMAGIC "Build with libmagic" "${WITH_LIBMAGIC}")
option(ODR_BUNDLE_ASSETS "Bundle assets during build and install" OFF)
option(ODR_PYTHON "Build Python bindings" OFF)
option(ODR_JNI "Build JNI bindings" OFF)

include(GNUInstallDirs)

Expand Down Expand Up @@ -410,6 +411,10 @@ if (ODR_PYTHON)
add_subdirectory("python")
endif ()

if (ODR_JNI)
add_subdirectory("jni")
endif ()

if (ODR_TEST)
add_subdirectory("test")
endif ()
Expand Down
5 changes: 4 additions & 1 deletion conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class OpenDocumentCoreConan(ConanFile):
"with_libmagic": [True, False],
"with_http_server": [True, False],
"with_python": [True, False],
"with_jni": [True, False],
"bundle_assets": [True, False],
}
default_options = {
Expand All @@ -32,10 +33,11 @@ class OpenDocumentCoreConan(ConanFile):
"with_libmagic": True,
"with_http_server": True,
"with_python": False,
"with_jni": False,
"bundle_assets": False,
}

exports_sources = ["cli/*", "cmake/*", "python/*", "resources/dist/*", "src/*", "CMakeLists.txt"]
exports_sources = ["cli/*", "cmake/*", "jni/*", "python/*", "resources/dist/*", "src/*", "CMakeLists.txt"]

def config_options(self):
if self.settings.os == "Windows":
Expand Down Expand Up @@ -84,6 +86,7 @@ def generate(self):
tc.variables["ODR_WITH_LIBMAGIC"] = self.options.get_safe("with_libmagic", False)
tc.variables["ODR_WITH_HTTP_SERVER"] = self.options.get_safe("with_http_server", False)
tc.variables["ODR_PYTHON"] = self.options.get_safe("with_python", False)
tc.variables["ODR_JNI"] = self.options.get_safe("with_jni", False)
tc.variables["ODR_BUNDLE_ASSETS"] = self.options.get_safe("bundle_assets", False)

# Get runenv info, exported by package_info() of dependencies
Expand Down
55 changes: 55 additions & 0 deletions jni/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AGENTS.md — JNI bindings

Hand-written JNI bindings for the public C++ API (`src/odr/*.hpp`), Java
package `app.opendocument.core`. Mirrors the surface of the python bindings
(`python/`); replaces the ad-hoc odr.droid `CoreWrapper`.

## Layout

| Path | What |
|------|------|
| `CMakeLists.txt` | Builds `libodr_jni` + `odr-core-java.jar`; included from the root build via `ODR_JNI`, or standalone against an installed `odrcore`. |
| `src/` | JNI sources, one `jni_*` unit per public-API area; `odr_jni.hpp` (strings, exceptions, handles) and `jni_convert.hpp` (struct/POJO marshalling) are the helpers. |
| `java/app/opendocument/core/` | Java API: enums, POJOs (styles, metas, `HtmlConfig`), and handle-backed wrappers extending `NativeResource`. |
| `tests/` | JUnit 5 suite, run via ctest (`odr_jni_junit`); inputs are generated inline (tmp files, zip-built minimal ODT) — no fixture files. |

## Design

- **Handle model**: a Java wrapper owns a heap-allocated copy of the C++ value
handle (`odr::DecodedFile`, `odr::Element`, ...) referenced by a `long`.
`NativeResource` frees it via `Cleaner`/`AutoCloseable`; each class has a
static `destroy(long)` native deleting the concrete C++ type.
- **Typed views are re-derived per call**: element/file handles always point to
the *base* type (`odr::Element`, `odr::DecodedFile`); natives of typed Java
classes call `as_paragraph()`/`as_text_file()` etc. on each call. Never store
a typed C++ subobject behind a base-typed handle (slicing/delete issues).
- **Keep-alive**: navigation results carry an `owner` reference
(`Element.owner()` → the `Document`) so the GC cannot free the root while
handles into it are alive — the JNI analogue of pyodr's `keep_alive`.
- **GC safety**: natives that use a handle are *instance* methods (the `this`
local reference keeps the wrapper — and its owner chain — reachable for the
duration of the call); only factories and `destroy` are static.
- **Enums cross as ordinals**: Java enum constant order must match the C++
enum declaration; `-1` encodes an absent `std::optional`.
- **Strings**: use `odr_jni::to_string`/`to_jstring` (real UTF-8 ↔ UTF-16),
never JNI's modified-UTF-8 `GetStringUTFChars`.
- **Exceptions**: every native body runs inside `odr_jni::guarded`; C++
exceptions map to `OdrException` subclasses (`odr_jni.cpp::throw_java`).
- Mirror the C++ names; drop the `Logger` parameters (bindings use the default
null logger).
- Stream-based C++ APIs (`write`, `save`, `pipe`) are bound as natives
returning `byte[]`/`String` via `std::ostringstream`.
- **Not bound**: `HtmlConfig::resource_locator` (function pointer across JNI);
the standard resource locator is always used. A `null` Java
`HtmlConfig.resourcePath` keeps the C++ default (the odr core data path) —
don't marshal it unconditionally.
- New public C++ API? Extend the matching `jni_*.cpp` + Java class and add a
JUnit test.
- C++ sources follow the repo clang-format; Java follows the
google-java-format style (2-space indent), no enforced formatter yet.
- Tests must stay hermetic: build inputs inline in `tests/.../TestFiles.java`;
HTML-rendering tests `assumeTrue(TestFiles.hasCoreData())` (skips when
assets are missing). Use `127.0.0.1`, not `localhost`, for the HTTP server
(the JVM prefers `::1`).
- Build/test loop: see `jni/README.md`; CI lives in
`.github/workflows/jni.yml`.
174 changes: 174 additions & 0 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# JNI bindings for OpenDocument.core (Java package `app.opendocument.core`).
#
# Included from the top-level CMakeLists.txt when `ODR_JNI` is ON. Can also be
# configured standalone against an installed `odrcore` package.

if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
cmake_minimum_required(VERSION 3.18)
project(odrjni LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

find_package(odrcore REQUIRED)
set(ODR_JNI_ODR_TARGET odrcore::odrcore)

# Same option name as the root build; an installed odrcore ships
# `http_server.hpp` only when it was built with the HTTP server.
option(ODR_WITH_HTTP_SERVER "Build with cpp-httplib HTTP server" ON)
else ()
set(ODR_JNI_ODR_TARGET odr)
endif ()

find_package(Java 11 REQUIRED COMPONENTS Development)
Comment thread
andiwand marked this conversation as resolved.
find_package(JNI REQUIRED)
include(UseJava)

add_library(odr_jni SHARED
"src/odr_jni.cpp"
"src/jni_core.cpp"
"src/jni_document.cpp"
"src/jni_file.cpp"
"src/jni_html.cpp"
"src/jni_http_server.cpp"
"src/jni_style.cpp"
)
target_include_directories(odr_jni PRIVATE ${JNI_INCLUDE_DIRS})
target_link_libraries(odr_jni PRIVATE ${ODR_JNI_ODR_TARGET})
if (ODR_WITH_HTTP_SERVER)
target_compile_definitions(odr_jni PRIVATE ODR_WITH_HTTP_SERVER)
endif ()

set(CMAKE_JAVA_COMPILE_FLAGS --release 17 -Xlint:all)

add_jar(odr_java
SOURCES
"java/app/opendocument/core/AnchorType.java"
"java/app/opendocument/core/Archive.java"
"java/app/opendocument/core/ArchiveFile.java"
"java/app/opendocument/core/Bookmark.java"
"java/app/opendocument/core/Circle.java"
"java/app/opendocument/core/Color.java"
"java/app/opendocument/core/CustomShape.java"
"java/app/opendocument/core/DecodePreference.java"
"java/app/opendocument/core/DecodedFile.java"
"java/app/opendocument/core/DecoderEngine.java"
"java/app/opendocument/core/DirectionalMeasure.java"
"java/app/opendocument/core/DirectionalString.java"
"java/app/opendocument/core/Document.java"
"java/app/opendocument/core/DocumentFile.java"
"java/app/opendocument/core/DocumentMeta.java"
"java/app/opendocument/core/DocumentPath.java"
"java/app/opendocument/core/DocumentType.java"
"java/app/opendocument/core/Element.java"
"java/app/opendocument/core/ElementType.java"
"java/app/opendocument/core/EncryptionState.java"
"java/app/opendocument/core/File.java"
"java/app/opendocument/core/FileCategory.java"
"java/app/opendocument/core/FileLocation.java"
"java/app/opendocument/core/FileMeta.java"
"java/app/opendocument/core/FileType.java"
"java/app/opendocument/core/FileWalker.java"
"java/app/opendocument/core/Filesystem.java"
"java/app/opendocument/core/FontFile.java"
"java/app/opendocument/core/FontPosition.java"
"java/app/opendocument/core/FontStyle.java"
"java/app/opendocument/core/FontWeight.java"
"java/app/opendocument/core/Frame.java"
"java/app/opendocument/core/GlobalParams.java"
"java/app/opendocument/core/GraphicStyle.java"
"java/app/opendocument/core/HorizontalAlign.java"
"java/app/opendocument/core/Html.java"
"java/app/opendocument/core/HtmlConfig.java"
"java/app/opendocument/core/HtmlPage.java"
"java/app/opendocument/core/HtmlResource.java"
"java/app/opendocument/core/HtmlResourceType.java"
"java/app/opendocument/core/HtmlService.java"
"java/app/opendocument/core/HtmlTableGridlines.java"
"java/app/opendocument/core/HtmlView.java"
"java/app/opendocument/core/HttpServer.java"
"java/app/opendocument/core/Image.java"
"java/app/opendocument/core/ImageFile.java"
"java/app/opendocument/core/Line.java"
"java/app/opendocument/core/LineBreak.java"
"java/app/opendocument/core/Link.java"
"java/app/opendocument/core/ListItem.java"
"java/app/opendocument/core/MasterPage.java"
"java/app/opendocument/core/Measure.java"
"java/app/opendocument/core/NativeLibrary.java"
"java/app/opendocument/core/NativeResource.java"
"java/app/opendocument/core/Odr.java"
"java/app/opendocument/core/OdrException.java"
"java/app/opendocument/core/Page.java"
"java/app/opendocument/core/PageLayout.java"
"java/app/opendocument/core/Paragraph.java"
"java/app/opendocument/core/ParagraphStyle.java"
"java/app/opendocument/core/PdfFile.java"
"java/app/opendocument/core/PdfTextMode.java"
"java/app/opendocument/core/PrintOrientation.java"
"java/app/opendocument/core/Rect.java"
"java/app/opendocument/core/Sheet.java"
"java/app/opendocument/core/SheetCell.java"
"java/app/opendocument/core/Slide.java"
"java/app/opendocument/core/Span.java"
"java/app/opendocument/core/Table.java"
"java/app/opendocument/core/TableCell.java"
"java/app/opendocument/core/TableCellStyle.java"
"java/app/opendocument/core/TableColumn.java"
"java/app/opendocument/core/TableColumnStyle.java"
"java/app/opendocument/core/TableDimensions.java"
"java/app/opendocument/core/TablePosition.java"
"java/app/opendocument/core/TableRow.java"
"java/app/opendocument/core/TableRowStyle.java"
"java/app/opendocument/core/TableStyle.java"
"java/app/opendocument/core/Text.java"
"java/app/opendocument/core/TextAlign.java"
"java/app/opendocument/core/TextFile.java"
"java/app/opendocument/core/TextRoot.java"
"java/app/opendocument/core/TextStyle.java"
"java/app/opendocument/core/TextWrap.java"
"java/app/opendocument/core/ValueType.java"
"java/app/opendocument/core/VerticalAlign.java"
OUTPUT_NAME odr-core-java
)

install(TARGETS odr_jni LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT jni)
install_jar(odr_java DESTINATION "${CMAKE_INSTALL_DATADIR}/java" COMPONENT jni)

if (ODR_TEST)
include(FetchContent)
FetchContent_Declare(junit_console
URL "https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar"
URL_HASH SHA256=a1de557821293ce903c213c694165fff532cf92081bac4238b9e05b35f04f43f
DOWNLOAD_NO_EXTRACT TRUE
)
FetchContent_MakeAvailable(junit_console)
set(ODR_JNI_JUNIT_JAR
"${junit_console_SOURCE_DIR}/junit-platform-console-standalone-1.10.2.jar")

add_jar(odr_java_tests
SOURCES
"tests/app/opendocument/core/DocumentTest.java"
"tests/app/opendocument/core/FileTest.java"
"tests/app/opendocument/core/HtmlTest.java"
"tests/app/opendocument/core/HttpServerTest.java"
"tests/app/opendocument/core/MetaTest.java"
"tests/app/opendocument/core/TestFiles.java"
INCLUDE_JARS odr_java "${ODR_JNI_JUNIT_JAR}"
OUTPUT_NAME odr-core-java-tests
)

enable_testing()
add_test(NAME odr_jni_junit
COMMAND "${Java_JAVA_EXECUTABLE}"
"-Djava.library.path=$<TARGET_FILE_DIR:odr_jni>"
-jar "${ODR_JNI_JUNIT_JAR}"
execute
--class-path "$<TARGET_PROPERTY:odr_java,JAR_FILE>"
--class-path "$<TARGET_PROPERTY:odr_java_tests,JAR_FILE>"
--scan-class-path
--fail-if-no-tests
)
set_tests_properties(odr_jni_junit PROPERTIES
ENVIRONMENT "ODR_CORE_DATA_PATH=${ODR_BUILD_ODR_DATA_PATH}")
endif ()
Loading
Loading