diff --git a/.bazelrc b/.bazelrc index bf57c0dd..22fc0c90 100644 --- a/.bazelrc +++ b/.bazelrc @@ -7,6 +7,17 @@ test --test_tag_filters=-e2e common --enable_platform_specific_config +# Java toolchain for the FFM bindings (//jni:*). JDK 25 is the hermetic remote +# JDK shipped by rules_java 9.6.1; being >= 22 it exposes java.lang.foreign as a +# stable (non-preview) API, so no --enable-preview is needed. These are +# unconditional build flags (they apply to every build), but only Java compile +# and run actions consult the Java toolchain, so they have no effect on the +# C++/WASM/Python targets. Per-test native-access flags live on the java_test. +build --java_language_version=25 +build --java_runtime_version=remotejdk_25 +build --tool_java_language_version=25 +build --tool_java_runtime_version=remotejdk_25 + # Platform-specific C++ standard flags build:macos --cxxopt=-std=c++20 build:macos --host_cxxopt=-std=c++20 diff --git a/MODULE.bazel b/MODULE.bazel index e3f186b0..2f3d1ba0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -5,6 +5,10 @@ module( bazel_dep(name = "rules_cc", version = "0.2.18") bazel_dep(name = "platforms", version = "1.1.0") +# copy_file (shell-free, cross-platform) for staging the native lib into the jar. +# Pin to the version the graph resolves (via rules_jvm_external) to avoid a +# direct-dependency mismatch warning on every invocation. +bazel_dep(name = "bazel_skylib", version = "1.9.0") bazel_dep(name = "googletest", version = "1.17.0.bcr.2") bazel_dep(name = "pybind11_bazel", version = "3.0.1") bazel_dep(name = "rules_python", version = "2.0.1") @@ -17,6 +21,15 @@ bazel_dep(name = "apple_support", version = "1.24.2") # WASM builds (//wasm:*, //web:dds_mvp_wasm). If you bump this version, # rebuild and run web/update_wasm.sh; see docs/wasm_build.md and patch_mvp_wasm.py. bazel_dep(name = "emsdk", version = "5.0.7") +# Java bindings (//jni:*). Provides the hermetic remote JDK used to build and run +# the FFM (java.lang.foreign) smoke test. rules_java 9.6.1 registers remotejdk_21 +# and remotejdk_25; we pin JDK 25 (>= 22, so the Foreign Function & Memory API is +# stable, not preview) via --java_*_version in .bazelrc. +bazel_dep(name = "rules_java", version = "9.6.1") +# Java packaging (//jni:dds_ffm_dist). Provides java_export -> a Maven-coordinate +# jar plus a generated .publish target that supports file:// local repos, used to +# install into ~/.m2 (Phase 1; no remote/public publishing yet). +bazel_dep(name = "rules_jvm_external", version = "7.0") llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm") llvm.toolchain( diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 800cc995..6f6ff2a0 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1,5 +1,5 @@ { - "lockFileVersion": 26, + "lockFileVersion": 28, "registryFileHashes": { "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", @@ -10,10 +10,13 @@ "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", + "https://bcr.bazel.build/modules/abseil-cpp/20250127.0/MODULE.bazel": "d1086e248cda6576862b4b3fe9ad76a214e08c189af5b42557a6e1888812c5d5", "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://bcr.bazel.build/modules/abseil-py/2.1.0/MODULE.bazel": "5ebe5bf853769c65707e5c28f216798f7a4b1042015e6a36e6d03094d94bec8a", + "https://bcr.bazel.build/modules/abseil-py/2.1.0/source.json": "0e8fc4f088ce07099c1cd6594c20c7ddbb48b4b3c0849b7d94ba94be88ff042b", "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", @@ -25,21 +28,25 @@ "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456", "https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838", + "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.13.0/MODULE.bazel": "c14c33c7c3c730612bdbe14ebbb5e61936b6f11322ea95a6e91cd1ba962f94df", "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", + "https://bcr.bazel.build/modules/bazel_features/1.25.0/MODULE.bazel": "e2e60a10a6da64bbf533f15ca652bf61a033e41c2ed734d79a9a08ba87f68c1a", "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", "https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e", + "https://bcr.bazel.build/modules/bazel_features/1.36.0/MODULE.bazel": "596cb62090b039caf1cad1d52a8bc35cf188ca9a4e279a828005e7ee49a1bec3", "https://bcr.bazel.build/modules/bazel_features/1.38.0/MODULE.bazel": "f9b8a9c890ebd216b4049fd12a31d3c2602e3403c7af636b04fbbd7453edc9c9", "https://bcr.bazel.build/modules/bazel_features/1.39.0/MODULE.bazel": "28739425c1fc283c91931619749c832b555e60bcd1010b40d8441ce0a5cf726d", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", @@ -66,12 +73,26 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.1/MODULE.bazel": "02a13b77321773b2042e70ee5e4c5e099c8ddee4cf2da9cd420442c36938d4bd", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.4/MODULE.bazel": "460aa12d01231a80cce03c548287b433b321d205b0028ae596728c35e5ee442e", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/MODULE.bazel": "396c1ef53835aafe3d42ce6619080531ee770648303731f16cfaa33fa056bf0c", + "https://bcr.bazel.build/modules/bazel_worker_api/0.0.8/source.json": "abaf8ac9d2ab2f47bda9af4c0c080ff7907378888e1f4bc62a0539dd13ba61e8", + "https://bcr.bazel.build/modules/bazel_worker_java/0.0.4/MODULE.bazel": "82494a01018bb7ef06d4a17ec4cd7a758721f10eb8b6c820a818e70d669500db", + "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/MODULE.bazel": "e76479eae70bd4e8f5f4c2dfc5d03ab971cfb18750246c7b3f3454c5c2ee6629", + "https://bcr.bazel.build/modules/bazel_worker_java/0.0.8/source.json": "9395c4679444bc47bf7e51a710366a4480aa371c6f6bed01868e2fabcf11acec", "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", "https://bcr.bazel.build/modules/emsdk/5.0.7/MODULE.bazel": "c9464a9a8c07bdef4bf2ddcd67c20bcb3c71915da818679756b1e85a04a0b9c0", "https://bcr.bazel.build/modules/emsdk/5.0.7/source.json": "0c4e55e4b706d842045b1b8583efc060945432722c81a8f9a7f0855c986d6321", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8", "https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2", + "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", + "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", + "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", + "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", + "https://bcr.bazel.build/modules/gazelle/0.40.0/MODULE.bazel": "42ba5378ebe845fca43989a53186ab436d956db498acde790685fe0e8f9c6146", + "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", @@ -92,7 +113,9 @@ "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", "https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92", "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", - "https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", @@ -108,25 +131,37 @@ "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", + "https://bcr.bazel.build/modules/protobuf/27.2/MODULE.bazel": "32450b50673882e4c8c3d10a83f3bc82161b213ed2f80d17e38bece8f165c295", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", + "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://bcr.bazel.build/modules/protobuf/29.3/MODULE.bazel": "77480eea5fb5541903e49683f24dc3e09f4a79e0eea247414887bb9fc0066e94", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/protobuf/31.1/MODULE.bazel": "379a389bb330b7b8c1cdf331cc90bf3e13de5614799b3b52cdb7c6f389f6b38e", "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", "https://bcr.bazel.build/modules/pybind11_bazel/2.13.6/MODULE.bazel": "2d746fda559464b253b2b2e6073cb51643a2ac79009ca02100ebbc44b4548656", + "https://bcr.bazel.build/modules/pybind11_bazel/3.0.0/MODULE.bazel": "a2bfa6020ed603a00d944161c63173c7f109774e99bee0c2cd8dbf24159f8134", "https://bcr.bazel.build/modules/pybind11_bazel/3.0.1/MODULE.bazel": "67899804c3dbdf169d107ce7c00e60a38a46d097c7ff3460bdebe192cbc77f56", "https://bcr.bazel.build/modules/pybind11_bazel/3.0.1/source.json": "5bcb3ae55c0d5a159bd910154d7ee8873bdf61dd7b13cd0a2bb456011ec706ae", "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", "https://bcr.bazel.build/modules/re2/2025-08-12.bcr.1/MODULE.bazel": "e09b434b122bfb786a69179f9b325e35cb1856c3f56a7a81dd61609260ed46e1", - "https://bcr.bazel.build/modules/re2/2025-08-12.bcr.1/source.json": "a8ae7c09533bf67f9f6e5122d884d5741600b09d78dca6fc0f2f8d2ee0c2d957", + "https://bcr.bazel.build/modules/re2/2025-11-05.bcr.1/MODULE.bazel": "3d9d4995833fc0334fc5c88b56a05288dd25d651544cd7b2233bbd6357bbeba0", + "https://bcr.bazel.build/modules/re2/2025-11-05.bcr.1/source.json": "7df1394aabda1c9bc188a302f5d54b1c657924edd04ebc57d2be29dbd7efd141", "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", - "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", + "https://bcr.bazel.build/modules/rules_android/0.6.4/MODULE.bazel": "b4cde12d506dd65d82b2be39761f49f5797303343a3d5b4ee191c0cdf9ef387c", + "https://bcr.bazel.build/modules/rules_android/0.7.1/MODULE.bazel": "a806fc382a774252f228a40e3b11b9fcc6276f8778c7fb33e9f72937c6258363", + "https://bcr.bazel.build/modules/rules_android/0.7.1/source.json": "151440aed3f0f73a00d4ed5cec5d31f63a6fef9b95d8fab1eb1810150fa525f2", "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", @@ -146,13 +181,23 @@ "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", + "https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8", "https://bcr.bazel.build/modules/rules_cc/0.2.16/MODULE.bazel": "9242fa89f950c6ef7702801ab53922e99c69b02310c39fb6e62b2bd30df2a1d4", "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", "https://bcr.bazel.build/modules/rules_cc/0.2.18/MODULE.bazel": "4460ec36adc8f722a6a2a4ac9374cb91f2acebadaa93fc37966129afb3dece87", "https://bcr.bazel.build/modules/rules_cc/0.2.18/source.json": "abad668ff2fd63ada1ac49bf386d37e27048b89a3465a6fd968bb832b00a09d3", "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", + "https://bcr.bazel.build/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1", "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", + "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", + "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", + "https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0", + "https://bcr.bazel.build/modules/rules_go/0.51.0-rc2/MODULE.bazel": "edfc3a9cea7bedb0eaaff37b0d7817c1a4bf72b3c615580b0ffcee6c52690fd4", + "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", + "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", + "https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", "https://bcr.bazel.build/modules/rules_java/6.3.0/MODULE.bazel": "a97c7678c19f236a956ad260d59c86e10a463badb7eb2eda787490f4c969b963", @@ -161,20 +206,32 @@ "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.4.0/MODULE.bazel": "a592852f8a3dd539e82ee6542013bf2cadfc4c6946be8941e189d224500a8934", "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", + "https://bcr.bazel.build/modules/rules_java/8.6.0/MODULE.bazel": "9c064c434606d75a086f15ade5edb514308cccd1544c2b2a89bbac4310e41c71", "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", + "https://bcr.bazel.build/modules/rules_java/8.6.3/MODULE.bazel": "e90505b7a931d194245ffcfb6ff4ca8ef9d46b4e830d12e64817752e0198e2ed", + "https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a", "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d", - "https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6", + "https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2", + "https://bcr.bazel.build/modules/rules_java/9.6.1/MODULE.bazel": "6b0b7172ce598e37e31d1e24f2a492a5249b88304bd25b02b465ee22e3aa3752", + "https://bcr.bazel.build/modules/rules_java/9.6.1/source.json": "d577c30fe3005821ac39c6ed43eb5accc77ff67c7b80f4043101aef4b027a903", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/6.2/MODULE.bazel": "36a6e52487a855f33cb960724eb56547fa87e2c98a0474c3acad94339d7f8e99", "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.6/MODULE.bazel": "153042249c7060536dc95b6bb9f9bb8063b8a0b0cb7acdb381bddbc2374aed55", "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", - "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4", + "https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274", + "https://bcr.bazel.build/modules/rules_jvm_external/7.0/MODULE.bazel": "421482bdbcf05709f933c96b867a599deb517f2804ceb3e74511880610cfbf71", + "https://bcr.bazel.build/modules/rules_jvm_external/7.0/source.json": "714cd003eadf5be5c83268311fe8e951db39f802babeaddc536b3560dc8f6faf", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.5/MODULE.bazel": "043a16a572f610558ec2030db3ff0c9938574e7dd9f58bded1bb07c0192ef025", "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", - "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", + "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/MODULE.bazel": "00d39c5e0fa78cd86193946265bb849e7878c24e44260f9525108428852b315c", + "https://bcr.bazel.build/modules/rules_kotlin/2.2.2/source.json": "7a32c2259c79ae0c9a036121f120de825e3ba5f0f3a209ffbbdccf4dc62489b9", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", @@ -187,7 +244,9 @@ "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", + "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", @@ -197,16 +256,23 @@ "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", "https://bcr.bazel.build/modules/rules_python/0.34.0/MODULE.bazel": "1d623d026e075b78c9fde483a889cda7996f5da4f36dffb24c246ab30f06513a", + "https://bcr.bazel.build/modules/rules_python/0.37.1/MODULE.bazel": "3faeb2d9fa0a81f8980643ee33f212308f4d93eea4b9ce6f36d0b742e71e9500", + "https://bcr.bazel.build/modules/rules_python/0.37.2/MODULE.bazel": "b5ffde91410745750b6c13be1c5dc4555ef5bc50562af4a89fd77807fdde626a", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", "https://bcr.bazel.build/modules/rules_python/1.5.1/MODULE.bazel": "acfe65880942d44a69129d4c5c3122d57baaf3edf58ae5a6bd4edea114906bf5", "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", + "https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f", "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", "https://bcr.bazel.build/modules/rules_python/1.8.4/MODULE.bazel": "33e3971e66161a3e955f7a0d411a8d1f291c4ce4c561851512466f3c77ff8ece", "https://bcr.bazel.build/modules/rules_python/1.9.0/MODULE.bazel": "afc3a05f29f09f2d3ee95ad99a145250dab41a2b2d8d6f82cc91936b3213282c", "https://bcr.bazel.build/modules/rules_python/2.0.1/MODULE.bazel": "294fbfe4dc3b24a925172e5487e1e8d3c2927cf4fba1eaddd7bad0ce50701da1", "https://bcr.bazel.build/modules/rules_python/2.0.1/source.json": "f2bacc75d3fc496dfd3e6a5986200b10e70169aaaf4ee5f1cea573c8d347e7c4", + "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/MODULE.bazel": "d44fec647d0aeb67b9f3b980cf68ba634976f3ae7ccd6c07d790b59b87a4f251", + "https://bcr.bazel.build/modules/rules_robolectric/4.14.1.2/source.json": "37c10335f2361c337c5c1f34ed36d2da70534c23088062b33a8bdaab68aa9dea", + "https://bcr.bazel.build/modules/rules_shell/0.1.2/MODULE.bazel": "66e4ca3ce084b04af0b9ff05ff14cab4e5df7503973818bb91cbc6cda08d32fc", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", @@ -229,6 +295,8 @@ "https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468", "https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c", "https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658", + "https://bcr.bazel.build/modules/toml.bzl/0.3.0/MODULE.bazel": "5016e5dd1ad2200e119a4b28b2b3935e276c4b480f2fe3e952bea7eeba88f578", + "https://bcr.bazel.build/modules/toml.bzl/0.3.0/source.json": "0cf7c878c419b37ddb55f3dd93dd7c0c409bd7c4efacb3da504e0748780b2fa9", "https://bcr.bazel.build/modules/toolchains_llvm/1.7.0/MODULE.bazel": "55aca6a8c5b372651f663c5e22faf3664d81165e40074c98fb8a30ee5af83272", "https://bcr.bazel.build/modules/toolchains_llvm/1.7.0/source.json": "cda5fa1abeab5561e811860222952f6cb9373f34b88c5b3f4417459dca057a6d", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", @@ -236,6 +304,7 @@ "https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072", "https://bcr.bazel.build/modules/yq.bzl/0.1.1/source.json": "2d2bad780a9f2b9195a4a370314d2c17ae95eaa745cefc2e12fbc49759b15aa3", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" @@ -260,7 +329,7 @@ }, "@@emsdk+//:emscripten_deps.bzl%emscripten_deps": { "general": { - "bzlTransitiveDigest": "Dt5IF0PG0xjU5iMLeU6FQ1/xWvBqUgUognNk3r5pJXY=", + "bzlTransitiveDigest": "F42/mZKtzkBQnTUD4cHY6FUWdyW5Gm0Z1zmvUVFI0NY=", "usagesDigest": "lqS0hMGr6MqmX63BGZOskMFcqzqO53K0UlYFxuE3QSU=", "recordedInputs": [ "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", @@ -330,63 +399,55 @@ } } }, - "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "@@rules_android+//bzlmod_extensions:apksig.bzl%apksig_extension": { "general": { - "bzlTransitiveDigest": "Ga4z8lQy1YQ5rAMy+dOl0dqcCEBnYNCXku8x3YQmDZI=", - "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", + "bzlTransitiveDigest": "qEcqhUuyYlPmvAtyYRqS+4Y7cRHA2Vho3JIBJZJJX+s=", + "usagesDigest": "zr/niBQ/s2fHozWAsg4vI70wAxcuFjG+QtM15qGkq9o=", "recordedInputs": [ - "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools" + "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" ], "generatedRepoSpecs": { - "com_github_jetbrains_kotlin_git": { - "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", - "attributes": { - "urls": [ - "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" - ], - "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" - } - }, - "com_github_jetbrains_kotlin": { - "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", - "attributes": { - "git_repository_name": "com_github_jetbrains_kotlin_git", - "compiler_version": "1.9.23" - } - }, - "com_github_google_ksp": { - "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", - "attributes": { - "urls": [ - "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" - ], - "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", - "strip_version": "1.9.23-1.0.20" - } - }, - "com_github_pinterest_ktlint": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", + "apksig": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", - "urls": [ - "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" - ], - "executable": true + "url": "https://android.googlesource.com/platform/tools/apksig/+archive/24e3075e68ebe17c0b529bb24bfda819db5e2f3b.tar.gz", + "build_file": "@@rules_android+//bzlmod_extensions:apksig.BUILD" } - }, - "rules_android": { + } + } + } + }, + "@@rules_android+//bzlmod_extensions:com_android_dex.bzl%com_android_dex_extension": { + "general": { + "bzlTransitiveDigest": "gTuOqknGgqPw9YQSCY+qXxgxVwiLRGdBCsCe4i4q/xw=", + "usagesDigest": "c1Y/KGGjUYCyd8zNIVTUh1bynVXRFz6xGKaSCBpQANM=", + "recordedInputs": [ + "REPO_MAPPING:rules_android+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "com_android_dex": { "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", "attributes": { - "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", - "strip_prefix": "rules_android-0.1.1", - "urls": [ - "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" - ] + "url": "https://android.googlesource.com/platform/dalvik/+archive/5a81c499a569731e2395f7c8d13c0e0d4e17a2b6.tar.gz", + "build_file": "@@rules_android+//bzlmod_extensions:com_android_dex.BUILD" } } } } }, + "@@rules_android+//rules/android_sdk_repository:rule.bzl%android_sdk_repository_extension": { + "general": { + "bzlTransitiveDigest": "+rMrzIrv7sImYmkbXJYv+gFpTJQ79X3MpwwMLI2A+oA=", + "usagesDigest": "iEGI2aNDMkHt9LXCdViLNUUOslpiVj2DrevWWXZEFnU=", + "recordedInputs": [], + "generatedRepoSpecs": { + "androidsdk": { + "repoRuleId": "@@rules_android+//rules/android_sdk_repository:rule.bzl%_android_sdk_repository", + "attributes": {} + } + } + } + }, "@@rules_nodejs+//nodejs:extensions.bzl%node": { "general": { "bzlTransitiveDigest": "4pUxCNc22K4I+6+4Nxu52Hur12tFRfa1JMsN5mdDv60=", @@ -550,6 +611,332 @@ } }, "facts": { + "@@rules_go+//go:extensions.bzl%go_sdk": { + "1.22.4": { + "aix_ppc64": [ + "go1.22.4.aix-ppc64.tar.gz", + "b9647fa9fc83a0cc5d4f092a19eaeaecf45f063a5aa7d4962fde65aeb7ae6ce1" + ], + "darwin_amd64": [ + "go1.22.4.darwin-amd64.tar.gz", + "c95967f50aa4ace34af0c236cbdb49a9a3e80ee2ad09d85775cb4462a5c19ed3" + ], + "darwin_arm64": [ + "go1.22.4.darwin-arm64.tar.gz", + "242b78dc4c8f3d5435d28a0d2cec9b4c1aa999b601fb8aa59fb4e5a1364bf827" + ], + "dragonfly_amd64": [ + "go1.22.4.dragonfly-amd64.tar.gz", + "f2fbb51af4719d3616efb482d6ed2b96579b474156f85a7ddc6f126764feec4b" + ], + "freebsd_386": [ + "go1.22.4.freebsd-386.tar.gz", + "7c54884bb9f274884651d41e61d1bc12738863ad1497e97ea19ad0e9aa6bf7b5" + ], + "freebsd_amd64": [ + "go1.22.4.freebsd-amd64.tar.gz", + "88d44500e1701dd35797619774d6dd51bf60f45a8338b0a82ddc018e4e63fb78" + ], + "freebsd_arm64": [ + "go1.22.4.freebsd-arm64.tar.gz", + "726dc093cf020277be45debf03c3b02b43c2efb3e2a5d4fba8f52579d65327dc" + ], + "freebsd_armv6l": [ + "go1.22.4.freebsd-arm.tar.gz", + "3d9efe47db142a22679aba46b1772e3900b0d87ae13bd2b3bc80dbf2ac0b2cd6" + ], + "freebsd_riscv64": [ + "go1.22.4.freebsd-riscv64.tar.gz", + "5f6b67e5e32f1d6ccb2d4dcb44934a5e2e870a877ba7443d86ec43cfc28afa71" + ], + "illumos_amd64": [ + "go1.22.4.illumos-amd64.tar.gz", + "d56ecc2f85b6418a21ef83879594d0c42ab4f65391a676bb12254870e6690d63" + ], + "linux_386": [ + "go1.22.4.linux-386.tar.gz", + "47a2a8d249a91eb8605c33bceec63aedda0441a43eac47b4721e3975ff916cec" + ], + "linux_amd64": [ + "go1.22.4.linux-amd64.tar.gz", + "ba79d4526102575196273416239cca418a651e049c2b099f3159db85e7bade7d" + ], + "linux_arm64": [ + "go1.22.4.linux-arm64.tar.gz", + "a8e177c354d2e4a1b61020aca3562e27ea3e8f8247eca3170e3fa1e0c2f9e771" + ], + "linux_armv6l": [ + "go1.22.4.linux-armv6l.tar.gz", + "e2b143fbacbc9cbd448e9ef41ac3981f0488ce849af1cf37e2341d09670661de" + ], + "linux_loong64": [ + "go1.22.4.linux-loong64.tar.gz", + "e2ff9436e4b34bf6926b06d97916e26d67a909a2effec17967245900f0816f1d" + ], + "linux_mips": [ + "go1.22.4.linux-mips.tar.gz", + "73f0dcc60458c4770593b05a7bc01cc0d31fc98f948c0c2334812c7a1f2fc3f1" + ], + "linux_mips64": [ + "go1.22.4.linux-mips64.tar.gz", + "417af97fc2630a647052375768be4c38adcc5af946352ea5b28613ea81ca5d45" + ], + "linux_mips64le": [ + "go1.22.4.linux-mips64le.tar.gz", + "7486e2d7dd8c98eb44df815ace35a7fe7f30b7c02326e3741bd934077508139b" + ], + "linux_mipsle": [ + "go1.22.4.linux-mipsle.tar.gz", + "69479c8aad301e459a8365b40cad1074a0dbba5defb9291669f94809c4c4be6e" + ], + "linux_ppc64": [ + "go1.22.4.linux-ppc64.tar.gz", + "dd238847e65bc3e2745caca475a5db6522a2fcf85cf6c38fc36a06642b19efd7" + ], + "linux_ppc64le": [ + "go1.22.4.linux-ppc64le.tar.gz", + "a3e5834657ef92523f570f798fed42f1f87bc18222a16815ec76b84169649ec4" + ], + "linux_riscv64": [ + "go1.22.4.linux-riscv64.tar.gz", + "56a827ff7dc6245bcd7a1e9288dffaa1d8b0fd7468562264c1523daf3b4f1b4a" + ], + "linux_s390x": [ + "go1.22.4.linux-s390x.tar.gz", + "7590c3e278e2dc6040aae0a39da3ca1eb2e3921673a7304cc34d588c45889eec" + ], + "netbsd_386": [ + "go1.22.4.netbsd-386.tar.gz", + "ddd2eebe34471a2502de6c5dad04ab27c9fc80cbde7a9ad5b3c66ecec4504e1d" + ], + "netbsd_amd64": [ + "go1.22.4.netbsd-amd64.tar.gz", + "33af79f6f935f6fbacc5d23876450b3567b79348fc065beef8e64081127dd234" + ], + "netbsd_arm64": [ + "go1.22.4.netbsd-arm64.tar.gz", + "c9a2971dec9f6d320c6f2b049b2353c6d0a2d35e87b8a4b2d78a2f0d62545f8e" + ], + "netbsd_armv6l": [ + "go1.22.4.netbsd-arm.tar.gz", + "fa3550ebd5375a70b3bcd342b5a71f4bd271dcbbfaf4eabefa2144ab5d8924b6" + ], + "openbsd_386": [ + "go1.22.4.openbsd-386.tar.gz", + "d21af022331bfdc2b5b161d616c3a1a4573d33cf7a30416ee509a8f3641deb47" + ], + "openbsd_amd64": [ + "go1.22.4.openbsd-amd64.tar.gz", + "72c0094c43f7e5722ec49c2a3e9dfa7a1123ac43a5f3a63eecf3e3795d3ff0ae" + ], + "openbsd_arm64": [ + "go1.22.4.openbsd-arm64.tar.gz", + "a7ab8d4e0b02bf06ed144ba42c61c0e93ee00f2b433415dfd4ad4b6e79f31650" + ], + "openbsd_armv6l": [ + "go1.22.4.openbsd-arm.tar.gz", + "1096831ea3c5ea3ca57d14251d9eda3786889531eb40d7d6775dcaa324d4b065" + ], + "openbsd_ppc64": [ + "go1.22.4.openbsd-ppc64.tar.gz", + "9716327c8a628358798898dc5148c49dbbeb5196bf2cbf088e550721a6e4f60b" + ], + "plan9_386": [ + "go1.22.4.plan9-386.tar.gz", + "a8dd4503c95c32a502a616ab78870a19889c9325fe9bd31eb16dd69346e4bfa8" + ], + "plan9_amd64": [ + "go1.22.4.plan9-amd64.tar.gz", + "5423a25808d76fe5aca8607a2e5ac5673abf45446b168cb5e9d8519ee9fe39a1" + ], + "plan9_armv6l": [ + "go1.22.4.plan9-arm.tar.gz", + "6af939ad583f5c85c09c53728ab7d38c3cc2b39167562d6c18a07c5c6608b370" + ], + "solaris_amd64": [ + "go1.22.4.solaris-amd64.tar.gz", + "e8cabe69c03085725afdb32a6f9998191a3e55a747b270d835fd05000d56abba" + ], + "windows_386": [ + "go1.22.4.windows-386.zip", + "aca4e2c37278a10f1c70dd0df142f7d66b50334fcee48978d409202d308d6d25" + ], + "windows_amd64": [ + "go1.22.4.windows-amd64.zip", + "26321c4d945a0035d8a5bc4a1965b0df401ff8ceac66ce2daadabf9030419a98" + ], + "windows_arm64": [ + "go1.22.4.windows-arm64.zip", + "8a2daa9ea28cbdafddc6171aefed384f4e5b6e714fb52116fe9ed25a132f37ed" + ], + "windows_armv6l": [ + "go1.22.4.windows-arm.zip", + "5fcd0671a49cecf39b41021621ee1b6e7aa1370f37122b72e80d4fd4185833b6" + ] + }, + "1.25.0": { + "aix_ppc64": [ + "go1.25.0.aix-ppc64.tar.gz", + "e5234a7dac67bc86c528fe9752fc9d63557918627707a733ab4cac1a6faed2d4" + ], + "darwin_amd64": [ + "go1.25.0.darwin-amd64.tar.gz", + "5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef" + ], + "darwin_arm64": [ + "go1.25.0.darwin-arm64.tar.gz", + "544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c" + ], + "dragonfly_amd64": [ + "go1.25.0.dragonfly-amd64.tar.gz", + "5ed3cf9a810a1483822538674f1336c06b51aa1b94d6d545a1a0319a48177120" + ], + "freebsd_386": [ + "go1.25.0.freebsd-386.tar.gz", + "abea5d5c6697e6b5c224731f2158fe87c602996a2a233ac0c4730cd57bf8374e" + ], + "freebsd_amd64": [ + "go1.25.0.freebsd-amd64.tar.gz", + "86e6fe0a29698d7601c4442052dac48bd58d532c51cccb8f1917df648138730b" + ], + "freebsd_arm": [ + "go1.25.0.freebsd-arm.tar.gz", + "d90b78e41921f72f30e8bbc81d9dec2cff7ff384a33d8d8debb24053e4336bfe" + ], + "freebsd_arm64": [ + "go1.25.0.freebsd-arm64.tar.gz", + "451d0da1affd886bfb291b7c63a6018527b269505db21ce6e14724f22ab0662e" + ], + "freebsd_riscv64": [ + "go1.25.0.freebsd-riscv64.tar.gz", + "7b565f76bd8bda46549eeaaefe0e53b251e644c230577290c0f66b1ecdb3cdbe" + ], + "illumos_amd64": [ + "go1.25.0.illumos-amd64.tar.gz", + "b1e1fdaab1ad25aa1c08d7a36c97d45d74b98b89c3f78c6d2145f77face54a2c" + ], + "linux_386": [ + "go1.25.0.linux-386.tar.gz", + "8c602dd9d99bc9453b3995d20ce4baf382cc50855900a0ece5de9929df4a993a" + ], + "linux_amd64": [ + "go1.25.0.linux-amd64.tar.gz", + "2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613" + ], + "linux_arm64": [ + "go1.25.0.linux-arm64.tar.gz", + "05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae" + ], + "linux_armv6l": [ + "go1.25.0.linux-armv6l.tar.gz", + "a5a8f8198fcf00e1e485b8ecef9ee020778bf32a408a4e8873371bfce458cd09" + ], + "linux_loong64": [ + "go1.25.0.linux-loong64.tar.gz", + "cab86b1cf761b1cb3bac86a8877cfc92e7b036fc0d3084123d77013d61432afc" + ], + "linux_mips": [ + "go1.25.0.linux-mips.tar.gz", + "d66b6fb74c3d91b9829dc95ec10ca1f047ef5e89332152f92e136cf0e2da5be1" + ], + "linux_mips64": [ + "go1.25.0.linux-mips64.tar.gz", + "4082e4381a8661bc2a839ff94ba3daf4f6cde20f8fb771b5b3d4762dc84198a2" + ], + "linux_mips64le": [ + "go1.25.0.linux-mips64le.tar.gz", + "70002c299ec7f7175ac2ef673b1b347eecfa54ae11f34416a6053c17f855afcc" + ], + "linux_mipsle": [ + "go1.25.0.linux-mipsle.tar.gz", + "b00a3a39eff099f6df9f1c7355bf28e4589d0586f42d7d4a394efb763d145a73" + ], + "linux_ppc64": [ + "go1.25.0.linux-ppc64.tar.gz", + "df166f33bd98160662560a72ff0b4ba731f969a80f088922bddcf566a88c1ec1" + ], + "linux_ppc64le": [ + "go1.25.0.linux-ppc64le.tar.gz", + "0f18a89e7576cf2c5fa0b487a1635d9bcbf843df5f110e9982c64df52a983ad0" + ], + "linux_riscv64": [ + "go1.25.0.linux-riscv64.tar.gz", + "c018ff74a2c48d55c8ca9b07c8e24163558ffec8bea08b326d6336905d956b67" + ], + "linux_s390x": [ + "go1.25.0.linux-s390x.tar.gz", + "34e5a2e19f2292fbaf8783e3a241e6e49689276aef6510a8060ea5ef54eee408" + ], + "netbsd_386": [ + "go1.25.0.netbsd-386.tar.gz", + "f8586cdb7aa855657609a5c5f6dbf523efa00c2bbd7c76d3936bec80aa6c0aba" + ], + "netbsd_amd64": [ + "go1.25.0.netbsd-amd64.tar.gz", + "ae8dc1469385b86a157a423bb56304ba45730de8a897615874f57dd096db2c2a" + ], + "netbsd_arm": [ + "go1.25.0.netbsd-arm.tar.gz", + "1ff7e4cc764425fc9dd6825eaee79d02b3c7cafffbb3691687c8d672ade76cb7" + ], + "netbsd_arm64": [ + "go1.25.0.netbsd-arm64.tar.gz", + "e1b310739f26724216aa6d7d7208c4031f9ff54c9b5b9a796ddc8bebcb4a5f16" + ], + "openbsd_386": [ + "go1.25.0.openbsd-386.tar.gz", + "4802a9b20e533da91adb84aab42e94aa56cfe3e5475d0550bed3385b182e69d8" + ], + "openbsd_amd64": [ + "go1.25.0.openbsd-amd64.tar.gz", + "c016cd984bebe317b19a4f297c4f50def120dc9788490540c89f28e42f1dabe1" + ], + "openbsd_arm": [ + "go1.25.0.openbsd-arm.tar.gz", + "a1e31d0bf22172ddde42edf5ec811ef81be43433df0948ece52fecb247ccfd8d" + ], + "openbsd_arm64": [ + "go1.25.0.openbsd-arm64.tar.gz", + "343ea8edd8c218196e15a859c6072d0dd3246fbbb168481ab665eb4c4140458d" + ], + "openbsd_ppc64": [ + "go1.25.0.openbsd-ppc64.tar.gz", + "694c14da1bcaeb5e3332d49bdc2b6d155067648f8fe1540c5de8f3cf8e157154" + ], + "openbsd_riscv64": [ + "go1.25.0.openbsd-riscv64.tar.gz", + "aa510ad25cf54c06cd9c70b6d80ded69cb20188ac6e1735655eef29ff7e7885f" + ], + "plan9_386": [ + "go1.25.0.plan9-386.tar.gz", + "46f8cef02086cf04bf186c5912776b56535178d4cb319cd19c9fdbdd29231986" + ], + "plan9_amd64": [ + "go1.25.0.plan9-amd64.tar.gz", + "29b34391d84095e44608a228f63f2f88113a37b74a79781353ec043dfbcb427b" + ], + "plan9_arm": [ + "go1.25.0.plan9-arm.tar.gz", + "0a047107d13ebe7943aaa6d54b1d7bbd2e45e68ce449b52915a818da715799c2" + ], + "solaris_amd64": [ + "go1.25.0.solaris-amd64.tar.gz", + "9977f9e4351984364a3b2b78f8b88bfd1d339812356d5237678514594b7d3611" + ], + "windows_386": [ + "go1.25.0.windows-386.zip", + "df9f39db82a803af0db639e3613a36681ab7a42866b1384b3f3a1045663961a7" + ], + "windows_amd64": [ + "go1.25.0.windows-amd64.zip", + "89efb4f9b30812eee083cc1770fdd2913c14d301064f6454851428f9707d190b" + ], + "windows_arm64": [ + "go1.25.0.windows-arm64.zip", + "27bab004c72b3d7bd05a69b6ec0fc54a309b4b78cc569dd963d8b3ec28bfdb8c" + ] + } + }, "@@rules_python+//python/extensions:pip.bzl%pip": { "dist_hashes": { "https://pypi.org/simple": { @@ -1044,5 +1431,6 @@ } } } - } + }, + "factsVersions": {} } diff --git a/docs/jni_interface.md b/docs/jni_interface.md new file mode 100644 index 00000000..03294b5d --- /dev/null +++ b/docs/jni_interface.md @@ -0,0 +1,217 @@ +# DDS on the JVM — Foreign Function & Memory (FFM) interface + +This document explains how to build the DDS native shared library and call the +Double Dummy Solver from Java using the **Foreign Function & Memory API** +(Project Panama, `java.lang.foreign`). No hand-written JNI C/C++ glue is +required: the JVM loads the plain C ABI directly. + +The bindings live under [`//jni`](../jni) and mirror how the other language +bindings work — see [python_interface.md](python_interface.md), +[dotnet_interface.md](dotnet_interface.md), and [wasm_build.md](wasm_build.md). + +## Table of Contents +1. [Prerequisites](#prerequisites) +2. [Building the shared library](#building-the-shared-library) +3. [The pure-C ABI shim](#the-pure-c-abi-shim) +4. [Using the FFM bindings](#using-the-ffm-bindings) +5. [Worked example](#worked-example) +6. [Loading and running](#loading-and-running) +7. [Packaging & local install](#packaging--local-install) +8. [Threading and lifecycle](#threading-and-lifecycle) +9. [Not yet supported](#not-yet-supported) + +--- + +## Prerequisites + +- **JDK 22 or later.** `java.lang.foreign` is a stable API from JDK 22, so no + `--enable-preview` is needed. The Bazel build is hermetic: `rules_java` + supplies the JDK (currently pinned to `remotejdk_25` in + [`.bazelrc`](../.bazelrc)); you do **not** need a system JDK installed. +- **Bazel 7.x or later**, as for the rest of the project. + +No `jextract` install is required — the bindings are hand-written (see +[Using the FFM bindings](#using-the-ffm-bindings)). + +## Building the shared library + +```bash +bazel build //jni:dds_shared +``` + +This produces one self-contained native library rolling up the entire solver +(all internal sub-libraries are linked in statically), named per OS: + +| OS | Artifact | Location | +| ------- | -------------------- | ------------------- | +| Linux | `libdds.so` | `bazel-bin/jni/` | +| macOS | `libdds.dylib` | `bazel-bin/jni/` | +| Windows | `dds.dll` | `bazel-bin/jni/` | + +Only the public C API is exported. Exports are constrained at link time by the +generated `jni/version_script.lds` (Linux) and `jni/exported_symbols.lds` +(macOS); on Windows the existing `__declspec(dllexport)` markers drive exports. +The lists are generated from the headers by `jni/gen_export_lists.py` and +verified by `//jni/tests:export_set_test`, which asserts the library exports +exactly the public symbols and leaks no internal C++ symbols. + +## The pure-C ABI shim + +The modern context API in `library/src/api/dds_api.hpp` takes C++ references and +C++ types (`const Deal&`, `SolverConfig`, `TTKind`), which are not addressable +from a pure-C FFI. The shim [`library/src/api/dds_c_api.h`](../library/src/api/dds_c_api.h) +wraps it with a clean, C-valid, pointer-only, POD-only surface that Java, .NET, +and ctypes can all bind to: + +- The solver handle is opaque: `typedef void* DDS_C_SOLVER_CTX`. +- Every struct is passed by pointer; no non-POD C++ type crosses the boundary. + +Shim entry points: `dds_c_create_solvercontext_default`, +`dds_c_destroy_solvercontext`, `dds_c_solve_board`, `dds_c_calc_dd_table`, +`dds_c_calc_par`. The flat legacy C API from `dll.h` (`SolveBoard`, +`CalcDDtable`, `GetDDSInfo`, `ErrorMessage`, …) is exported unchanged and is +also callable from FFM. + +## Using the FFM bindings + +`//jni:dds_ffm` provides the Java bindings in package `org.dds.ffm` +([`Dds.java`](../jni/java/org/dds/ffm/Dds.java)). They are **hand-written** +rather than generated by `jextract`: `jextract` is distributed only as +non-hermetic early-access binaries, and the generated output is just plain +`java.lang.foreign` code, so the bindings are checked in directly. `Dds` +exposes: + +- Public `MemoryLayout` constants matching the C structs: `DEAL`, + `FUTURE_TRICKS`, `DD_TABLE_DEAL`, `DD_TABLE_RESULTS`, `DDS_INFO`. +- Typed downcall wrappers for the shim functions plus `getDdsInfo`. +- `Dds.load(Path)` — loads the library via `SymbolLookup.libraryLookup` over an + owned `Arena`; `Dds` is `AutoCloseable`. + +Depend on it from your own `java_library`/`java_binary`: + +```python +java_binary( + name = "my_app", + srcs = ["MyApp.java"], + deps = ["//jni:dds_ffm"], + data = ["//jni:dds_shared"], + jvm_flags = ["--enable-native-access=ALL-UNNAMED"], +) +``` + +## Worked example + +Solve a single board (North holds all spades with spades trump — 13 tricks): + +```java +import static java.lang.foreign.ValueLayout.JAVA_INT; +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout.PathElement; +import java.lang.foreign.MemorySegment; +import java.nio.file.Path; +import org.dds.ffm.Dds; + +try (Dds dds = Dds.load(Path.of(System.getProperty("dds.library.path"))); + Arena arena = Arena.ofConfined()) { + + long remain = Dds.DEAL.byteOffset(PathElement.groupElement("remainCards")); + MemorySegment deal = arena.allocate(Dds.DEAL); + deal.fill((byte) 0); // allocate() is not guaranteed to zero the memory + // trump = spades (0), first = North (0) are now 0. + deal.set(JAVA_INT, remain + 0L * 4, 0x7FFC); // North spades + deal.set(JAVA_INT, remain + 5L * 4, 0x7FFC); // East hearts + deal.set(JAVA_INT, remain + 10L * 4, 0x7FFC); // South diamonds + deal.set(JAVA_INT, remain + 15L * 4, 0x7FFC); // West clubs + + MemorySegment ctx = dds.createSolverContext(); + try { + MemorySegment fut = arena.allocate(Dds.FUTURE_TRICKS); + int rc = dds.solveBoard(ctx, deal, -1, 1, 1, fut); // target, solutions, mode + long score = Dds.FUTURE_TRICKS.byteOffset(PathElement.groupElement("score")); + System.out.println("tricks = " + fut.get(JAVA_INT, score)); // 13 + } finally { + dds.destroySolverContext(ctx); + } +} +``` + +`remainCards` is a `[hand][suit]` array with 4 suits per hand, so element +`[hand][suit]` is at index `hand * 4 + suit`. Holdings are 13-bit rank masks +(`0x7FFC` = all ranks 2..A). See +[`DdsSmokeTest.java`](../jni/java/org/dds/ffm/DdsSmokeTest.java) for a complete, +tested program including `GetDDSInfo`. + +## Loading and running + +There are two ways to load the native library: + +- **`Dds.loadEmbedded()`** (default) — extracts the library bundled in the jar + for the current OS/arch (from the classpath resource + `native/-/`) to a temp file and loads it. Zero configuration; + this is what the published jar (below) is for. Host-platform only for now. +- **`Dds.load(Path)`** — loads a library at an explicit filesystem path via + `SymbolLookup.libraryLookup`, with no reliance on `System.loadLibrary` / + `java.library.path`. Useful when you built `//jni:dds_shared` yourself; in + Bazel, make it a `data` dependency and pass its runfiles path, e.g. + `-Ddds.library.path=$(rootpath //jni:dds_shared)`. + +FFM downcalls are restricted native operations. Run with +`--enable-native-access=ALL-UNNAMED` (as a `jvm_flag` or on the `java` +command line) to grant access and silence the runtime warning. + +The end-to-end smoke tests wire all of this together: + +```bash +bazel test //jni:dds_ffm_smoke_test # explicit-path loading +bazel test //jni:dds_ffm_embedded_smoke_test # loadEmbedded() from the jar +``` + +## Packaging & local install + +`//jni:dds_ffm_dist` is a `java_export` producing a Maven-coordinate jar, +`org.dds:dds-ffm:3.0.1`, that **embeds the host-platform native library** under +`native/-/` — so a consumer needs only the jar and +`Dds.loadEmbedded()`, no separate `.so`/`.dylib`/`.dll` to locate. + +Install it into your local Maven repository (`~/.m2`): + +```bash +bazel run //jni:dds_ffm_dist.publish \ + --define maven_repo="file://$HOME/.m2/repository" \ + --define gpg_sign=false +``` + +This deposits the main jar plus `-sources`, `-javadoc`, and the generated POM +under `org/dds/dds-ffm/3.0.1/`. Another project on the same machine can then +depend on it: + +```kotlin +implementation("org.dds:dds-ffm:3.0.1") // Gradle +``` +```xml +org.ddsdds-ffm3.0.1 +``` + +The bundled native library is **host-platform only** — the jar built on macOS +contains the macOS `.dylib` and so on. A single multi-OS/arch ("fat") jar and +publishing to a public/remote repository are not yet available (see below). + +## Threading and lifecycle + +Each `DDS_C_SOLVER_CTX` owns per-context solver state and its own transposition +table. A context is **not** thread-safe: use one solver context per thread. +Always release a context with `destroySolverContext` (the example above does so +in a `finally` block). Because the modern context API owns its state +per-handle, no global `InitializeStaticMemory` call is required before solving +through the shim. + +## Not yet supported + +- **Remote/public publishing** — only local `~/.m2` installs are wired today. + Publishing to Maven Central (OSSRH + GPG signing) or GitHub Packages is not + yet set up. +- **Multi-OS/arch "fat" jar** — the jar bundles only the host platform's native + library. Producing one jar with every triplet requires a CI matrix building + each platform and a final assembly step. +- **Hand-written JNI convenience API** (idiomatic `native`-method Java classes) + — deferred; the FFM path above is the supported route. diff --git a/jni/BUILD.bazel b/jni/BUILD.bazel new file mode 100644 index 00000000..77f24832 --- /dev/null +++ b/jni/BUILD.bazel @@ -0,0 +1,176 @@ +load("@bazel_skylib//rules:copy_file.bzl", "copy_file") +load("@rules_cc//cc:defs.bzl", "cc_shared_library") +load("@rules_java//java:defs.bzl", "java_library", "java_test") +load("@rules_jvm_external//:defs.bzl", "java_export") +load("@rules_python//python:defs.bzl", "py_library") + +# Single source of truth for the published Maven version. Keep in sync with the +# module() version in MODULE.bazel; the embedded smoke test cross-checks its +# major.minor against the library's GetDDSInfo so a gross divergence is caught. +DDS_FFM_VERSION = "3.0.1" + +# Host os+cpu combinations we can package a native library for. The genrule that +# stages the library encodes - in its output path, which must match the +# arch Dds.loadEmbedded() resolves at runtime (os.arch) — so the arch is part of +# the build config here, not hardcoded per-OS. +[ + config_setting( + name = name, + constraint_values = cvs, + ) + for name, cvs in { + "macos_arm64": ["@platforms//os:osx", "@platforms//cpu:arm64"], + "macos_x86_64": ["@platforms//os:osx", "@platforms//cpu:x86_64"], + "linux_arm64": ["@platforms//os:linux", "@platforms//cpu:arm64"], + "linux_x86_64": ["@platforms//os:linux", "@platforms//cpu:x86_64"], + "windows_x86_64": ["@platforms//os:windows", "@platforms//cpu:x86_64"], + }.items() +] + +# Export lists that constrain the shared library's ABI to the public C API. +# Regenerate with gen_export_lists.py after changing dll.h or dds_c_api.h; the +# checked-in .lds files are the source of truth consumed at link time. +exports_files([ + "version_script.lds", + "exported_symbols.lds", + "gen_export_lists.py", +]) + +# Importable form of the export-list generator so its parser can be unit-tested. +py_library( + name = "gen_export_lists_lib", + srcs = ["gen_export_lists.py"], + imports = ["."], + visibility = ["//jni:__subpackages__"], +) + +# One self-contained native shared library exporting the stable C ABI +# (dll.h flat API + dds_c_* shim). All internal sub-libraries are linked in +# statically, so the JVM's System.loadLibrary("dds") needs only this one file. +cc_shared_library( + name = "dds_shared", + deps = [ + "//library/src:dds", + "//library/src/api:dds_c_api", + ], + shared_lib_name = select({ + "//:build_windows": "dds.dll", + "//:build_macos": "libdds.dylib", + "//conditions:default": "libdds.so", + }), + additional_linker_inputs = select({ + "//:build_macos": [":exported_symbols.lds"], + "//:build_windows": [], + "//conditions:default": [":version_script.lds"], + }), + user_link_flags = select({ + "//:build_macos": ["-Wl,-exported_symbols_list,$(location :exported_symbols.lds)"], + "//:build_windows": [], + "//conditions:default": ["-Wl,--version-script=$(location :version_script.lds)"], + }), + visibility = ["//visibility:public"], +) + +# Hand-written Foreign Function & Memory (Project Panama) bindings for the shared +# library: struct MemoryLayouts and Linker downcall handles for the dds_c_* shim +# plus GetDDSInfo. Requires the JDK 22+ toolchain configured in .bazelrc. +java_library( + name = "dds_ffm", + srcs = ["java/org/dds/ffm/Dds.java"], + visibility = ["//visibility:public"], +) + +# Stage the host shared library under native/-/ for jar embedding. +# copy_file outputs are not configurable, so there is one per os+arch triplet +# gated by target_compatible_with; only the host's is analysed, and its output +# path's arch matches what Dds.loadEmbedded() looks up at runtime. copy_file is +# shell-free, so no cp/copy toolchain assumption on any host (incl. Windows). +# Host platform only (Phase 1) — a multi-arch fat jar is a separate follow-up. +[ + copy_file( + name = "native_in_jar_" + triplet.replace("-", "_"), + src = "//jni:dds_shared", + out = "native/" + triplet + "/" + libname, + target_compatible_with = cvs, + ) + for triplet, libname, cvs in [ + ("macos-aarch64", "libdds.dylib", ["@platforms//os:osx", "@platforms//cpu:arm64"]), + ("macos-x86_64", "libdds.dylib", ["@platforms//os:osx", "@platforms//cpu:x86_64"]), + ("linux-aarch64", "libdds.so", ["@platforms//os:linux", "@platforms//cpu:arm64"]), + ("linux-x86_64", "libdds.so", ["@platforms//os:linux", "@platforms//cpu:x86_64"]), + ("windows-x86_64", "dds.dll", ["@platforms//os:windows", "@platforms//cpu:x86_64"]), + ] +] + +# Publishable Maven jar: the dds_ffm binding classes (exported, not recompiled) +# plus the embedded native library extracted at runtime by Dds.loadEmbedded(). +# java_export also generates the runnable //jni:dds_ffm_dist.publish target +# (supports file:// local ~/.m2). The native resource is selected by host +# os+arch so its embedded path matches loadEmbedded()'s lookup; on any other +# platform the target is incompatible (skipped by //...) rather than a hard +# "no matching conditions" failure that would poison unrelated builds. +java_export( + name = "dds_ffm_dist", + exports = [":dds_ffm"], + maven_coordinates = "org.dds:dds-ffm:" + DDS_FFM_VERSION, + resource_strip_prefix = "jni", + resources = select({ + ":macos_arm64": [":native_in_jar_macos_aarch64"], + ":macos_x86_64": [":native_in_jar_macos_x86_64"], + ":linux_arm64": [":native_in_jar_linux_aarch64"], + ":linux_x86_64": [":native_in_jar_linux_x86_64"], + ":windows_x86_64": [":native_in_jar_windows_x86_64"], + "//conditions:default": [], + }), + target_compatible_with = select({ + ":macos_arm64": [], + ":macos_x86_64": [], + ":linux_arm64": [], + ":linux_x86_64": [], + ":windows_x86_64": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + visibility = ["//visibility:public"], +) + +# End-to-end FFM smoke test: loads the shared library and solves a known deal +# through the shim. Runs as a plain main (no JUnit dep). The shared lib is a +# data dep; its runfiles path is passed via -Ddds.library.path, and native +# access is enabled for the FFM downcalls. Off Windows (nm-free / dll path). +java_test( + name = "dds_ffm_smoke_test", + srcs = ["java/org/dds/ffm/DdsSmokeTest.java"], + main_class = "org.dds.ffm.DdsSmokeTest", + use_testrunner = False, + data = ["//jni:dds_shared"], + jvm_flags = [ + "--enable-native-access=ALL-UNNAMED", + "-Ddds.library.path=$(rootpath //jni:dds_shared)", + ], + deps = [":dds_ffm"], + target_compatible_with = select({ + "//:build_windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) + +# Validates the packaging path: loads the native lib via Dds.loadEmbedded() from +# the jar's classpath resource (depends on dds_ffm_dist, which embeds it) and +# solves the known deal. Off Windows like the other jni tests. +java_test( + name = "dds_ffm_embedded_smoke_test", + srcs = ["java/org/dds/ffm/DdsEmbeddedSmokeTest.java"], + main_class = "org.dds.ffm.DdsEmbeddedSmokeTest", + use_testrunner = False, + jvm_flags = [ + "--enable-native-access=ALL-UNNAMED", + # Cross-check the coordinate's major.minor against the embedded library's + # GetDDSInfo so a published version can't silently misrepresent it. + "-Ddds.coordinate.version=" + DDS_FFM_VERSION, + ], + deps = [":dds_ffm_dist"], + target_compatible_with = select({ + "//:build_windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) diff --git a/jni/exported_symbols.lds b/jni/exported_symbols.lds new file mode 100644 index 00000000..1e4e0980 --- /dev/null +++ b/jni/exported_symbols.lds @@ -0,0 +1,44 @@ +_AnalyseAllPlaysBin +_AnalyseAllPlaysPBN +_AnalysePlayBin +_AnalysePlayPBN +_CalcAllTables +_CalcAllTablesN +_CalcAllTablesPBN +_CalcAllTablesPBNN +_CalcDDtable +_CalcDDtableN +_CalcDDtablePBN +_CalcDDtablePBNN +_CalcPar +_CalcParPBN +_ConvertToDealerTextFormat +_ConvertToSidesTextFormat +_DealerPar +_DealerParBin +_ErrorMessage +_FreeMemory +_GetDDSInfo +_InitializeStaticMemory +_Par +_SetMaxThreads +_SetResources +_SetThreading +_SidesPar +_SidesParBin +_SolveAllBoards +_SolveAllBoardsBin +_SolveAllBoardsBinN +_SolveAllBoardsBinSeq +_SolveAllBoardsN +_SolveAllBoardsSeq +_SolveAllChunks +_SolveAllChunksBin +_SolveAllChunksPBN +_SolveBoard +_SolveBoardPBN +_dds_c_calc_dd_table +_dds_c_calc_par +_dds_c_create_solvercontext_default +_dds_c_destroy_solvercontext +_dds_c_solve_board diff --git a/jni/gen_export_lists.py b/jni/gen_export_lists.py new file mode 100644 index 00000000..13e12082 --- /dev/null +++ b/jni/gen_export_lists.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Generate linker export lists for the DDS shared library. + +Parses the public C-ABI headers (dll.h, dds_c_api.h) for their exported +function symbols and emits: + + * a Linux version script (--version-script) with the symbols under `global:` + and `local: *;` for everything else, and + * a macOS exported-symbols list (-exported_symbols_list) with each symbol + prefixed by a leading underscore. + +Windows needs no file: exports there are driven by __declspec(dllexport). + +The two lists are derived from the same parsed set so they cannot drift. Run +via Bazel genrule or manually; output is deterministic (symbols sorted). +""" + +import argparse +import re +import sys + +# A public export is a non-preprocessor, non-comment line that mentions the +# DLLEXPORT marker and declares a function: capture the identifier that +# immediately precedes the opening parenthesis. This handles both the +# trailing-return `auto STDCALL Name(` form in dll.h and the plain +# `Type dds_c_name(` form in dds_c_api.h. +_DECL_RE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(") + + +def parse_symbols(paths): + symbols = set() + for path in paths: + with open(path, encoding="utf-8") as handle: + for raw in handle: + line = raw.strip() + if not line or line.startswith("#") or line.startswith("//"): + continue + if "DLLEXPORT" not in line: + continue + match = _DECL_RE.search(line) + if not match: + continue + name = match.group(1) + # Skip the macro tokens themselves and the STDCALL marker. + if name in ("DLLEXPORT", "STDCALL", "EXTERN_C"): + continue + symbols.add(name) + return sorted(symbols) + + +def write_version_script(symbols, path): + lines = ["{", " global:"] + lines += [f" {name};" for name in symbols] + lines += [" local:", " *;", "};", ""] + with open(path, "w", encoding="utf-8") as handle: + handle.write("\n".join(lines)) + + +def write_exported_symbols(symbols, path): + lines = [f"_{name}" for name in symbols] + lines.append("") + with open(path, "w", encoding="utf-8") as handle: + handle.write("\n".join(lines)) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--headers", nargs="+", required=True, + help="Public C-ABI headers to scan for exported symbols.", + ) + parser.add_argument("--linux", help="Output path for the Linux version script.") + parser.add_argument("--macos", help="Output path for the macOS exported-symbols list.") + args = parser.parse_args(argv) + + symbols = parse_symbols(args.headers) + if not symbols: + print("error: no exported symbols found", file=sys.stderr) + return 1 + + if args.linux: + write_version_script(symbols, args.linux) + if args.macos: + write_exported_symbols(symbols, args.macos) + if not args.linux and not args.macos: + # No outputs requested: print both to stdout for inspection. + print("# symbols:", *symbols, sep="\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/jni/java/org/dds/ffm/Dds.java b/jni/java/org/dds/ffm/Dds.java new file mode 100644 index 00000000..ff5019b2 --- /dev/null +++ b/jni/java/org/dds/ffm/Dds.java @@ -0,0 +1,289 @@ +/* + DDS, a bridge double dummy solver. + + Java Foreign Function & Memory (Project Panama) bindings for the DDS native + shared library. These bind the pure-C ABI shim (dds_c_*) plus GetDDSInfo from + the flat C API. Requires JDK 22+ (java.lang.foreign is stable there). + + The bindings are hand-written rather than jextract-generated: jextract ships + only as non-hermetic early-access binaries, whereas this file needs nothing + beyond the JDK. It mirrors exactly what jextract would emit — struct + MemoryLayouts and Linker downcall handles. + + See LICENSE and README. +*/ + +package org.dds.ffm; + +import static java.lang.foreign.ValueLayout.ADDRESS; +import static java.lang.foreign.ValueLayout.JAVA_BYTE; +import static java.lang.foreign.ValueLayout.JAVA_INT; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.invoke.MethodHandle; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * FFM bindings for the DDS native shared library. Load one instance per library + * file with {@link #load(Path)}; it owns an {@link Arena} scoping the native + * library and any segments allocated from {@link #arena()}. Not thread-safe: + * use one native SolverContext per thread. + */ +public final class Dds implements AutoCloseable { + + // ---- Struct memory layouts (match library/src/api/dll.h exactly) ---- + + /** struct Deal — DDS_HANDS = DDS_SUITS = 4. */ + public static final MemoryLayout DEAL = MemoryLayout.structLayout( + JAVA_INT.withName("trump"), + JAVA_INT.withName("first"), + MemoryLayout.sequenceLayout(3, JAVA_INT).withName("currentTrickSuit"), + MemoryLayout.sequenceLayout(3, JAVA_INT).withName("currentTrickRank"), + MemoryLayout.sequenceLayout(16, JAVA_INT).withName("remainCards")) + .withName("Deal"); + + /** struct FutureTricks. */ + public static final MemoryLayout FUTURE_TRICKS = MemoryLayout.structLayout( + JAVA_INT.withName("nodes"), + JAVA_INT.withName("cards"), + MemoryLayout.sequenceLayout(13, JAVA_INT).withName("suit"), + MemoryLayout.sequenceLayout(13, JAVA_INT).withName("rank"), + MemoryLayout.sequenceLayout(13, JAVA_INT).withName("equals"), + MemoryLayout.sequenceLayout(13, JAVA_INT).withName("score")) + .withName("FutureTricks"); + + /** struct DdTableDeal. */ + public static final MemoryLayout DD_TABLE_DEAL = MemoryLayout.structLayout( + MemoryLayout.sequenceLayout(16, JAVA_INT).withName("cards")) + .withName("DdTableDeal"); + + /** struct DdTableResults — res_table[DDS_STRAINS][DDS_HANDS] = 5x4. */ + public static final MemoryLayout DD_TABLE_RESULTS = MemoryLayout.structLayout( + MemoryLayout.sequenceLayout(20, JAVA_INT).withName("resTable")) + .withName("DdTableResults"); + + /** struct ParResults — par_score[2][16], par_contracts_string[2][128]. */ + public static final MemoryLayout PAR_RESULTS = MemoryLayout.structLayout( + MemoryLayout.sequenceLayout(2 * 16, JAVA_BYTE).withName("parScore"), + MemoryLayout.sequenceLayout(2 * 128, JAVA_BYTE).withName("parContractsString")) + .withName("ParResults"); + + /** struct DDSInfo. The char[10] version_string forces 2 bytes of padding. */ + public static final MemoryLayout DDS_INFO = MemoryLayout.structLayout( + JAVA_INT.withName("major"), + JAVA_INT.withName("minor"), + JAVA_INT.withName("patch"), + MemoryLayout.sequenceLayout(10, JAVA_BYTE).withName("versionString"), + MemoryLayout.paddingLayout(2), + JAVA_INT.withName("system"), + JAVA_INT.withName("numBits"), + JAVA_INT.withName("compiler"), + JAVA_INT.withName("constructor"), + JAVA_INT.withName("numCores"), + JAVA_INT.withName("threading"), + JAVA_INT.withName("noOfThreads"), + MemoryLayout.sequenceLayout(128, JAVA_BYTE).withName("threadSizes"), + MemoryLayout.sequenceLayout(1024, JAVA_BYTE).withName("systemString")) + .withName("DDSInfo"); + + private final Arena arena; + private final MethodHandle getDdsInfo; + private final MethodHandle createContext; + private final MethodHandle destroyContext; + private final MethodHandle solveBoard; + private final MethodHandle calcDdTable; + private final MethodHandle calcPar; + + private Dds(Arena arena, SymbolLookup lookup) { + this.arena = arena; + Linker linker = Linker.nativeLinker(); + this.getDdsInfo = handle(linker, lookup, "GetDDSInfo", + FunctionDescriptor.ofVoid(ADDRESS)); + this.createContext = handle(linker, lookup, "dds_c_create_solvercontext_default", + FunctionDescriptor.of(ADDRESS)); + this.destroyContext = handle(linker, lookup, "dds_c_destroy_solvercontext", + FunctionDescriptor.ofVoid(ADDRESS)); + this.solveBoard = handle(linker, lookup, "dds_c_solve_board", + FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_INT, JAVA_INT, JAVA_INT, ADDRESS)); + this.calcDdTable = handle(linker, lookup, "dds_c_calc_dd_table", + FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS)); + this.calcPar = handle(linker, lookup, "dds_c_calc_par", + FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_INT, ADDRESS, ADDRESS)); + } + + private static MethodHandle handle(Linker linker, SymbolLookup lookup, String name, + FunctionDescriptor descriptor) { + MemorySegment symbol = lookup.find(name) + .orElseThrow(() -> new IllegalStateException("symbol not found: " + name)); + return linker.downcallHandle(symbol, descriptor); + } + + /** Load the DDS shared library at {@code libraryFile} and bind its symbols. */ + public static Dds load(Path libraryFile) { + Arena arena = Arena.ofShared(); + try { + SymbolLookup lookup = SymbolLookup.libraryLookup(libraryFile, arena); + return new Dds(arena, lookup); + } catch (RuntimeException | Error e) { + // A shared arena is not auto-managed; close it so a failed load (e.g. + // wrong-arch/missing library, or a missing symbol) does not leak the + // native memory session for the JVM lifetime. + arena.close(); + throw e; + } + } + + /** + * Load the DDS library bundled inside this jar for the current OS/arch, + * extracting it to a temp file first. Requires the native library to be on + * the classpath at {@code /native/-/} (host platform only). + */ + public static Dds loadEmbedded() { + String os = osToken(); + String arch = archToken(); + String lib = libFileName(os); + String resource = "/native/" + os + "-" + arch + "/" + lib; + try (InputStream in = Dds.class.getResourceAsStream(resource)) { + if (in == null) { + throw new IllegalStateException("no embedded DDS library at " + resource); + } + String suffix = lib.substring(lib.lastIndexOf('.')); + String extractDir = System.getProperty("dds.extract.dir"); + Path tmp = (extractDir == null || extractDir.isBlank()) + ? Files.createTempFile("dds", suffix) + : Files.createTempFile(Path.of(extractDir), "dds", suffix); + Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); + tmp.toFile().deleteOnExit(); + return load(tmp); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String osToken() { + String name = System.getProperty("os.name", "").toLowerCase(); + if (name.contains("mac") || name.contains("darwin")) { + return "macos"; + } + if (name.contains("win")) { + return "windows"; + } + if (name.contains("linux")) { + return "linux"; + } + throw new UnsupportedOperationException( + "unsupported OS for embedded DDS library: " + System.getProperty("os.name")); + } + + private static String archToken() { + String arch = System.getProperty("os.arch", "").toLowerCase(); + if (arch.equals("aarch64") || arch.equals("arm64")) { + return "aarch64"; + } + if (arch.equals("x86_64") || arch.equals("amd64")) { + return "x86_64"; + } + throw new UnsupportedOperationException( + "unsupported architecture for embedded DDS library: " + System.getProperty("os.arch")); + } + + private static String libFileName(String os) { + switch (os) { + case "macos": + return "libdds.dylib"; + case "windows": + return "dds.dll"; + default: + return "libdds.so"; + } + } + + /** Arena scoping this library; use it to allocate struct segments. */ + public Arena arena() { + return arena; + } + + /** Fill {@code info} (a {@link #DDS_INFO} segment) with library metadata. */ + public void getDdsInfo(MemorySegment info) { + invokeVoid(getDdsInfo, info); + } + + /** Create a default solver context; returns an opaque native handle. */ + public MemorySegment createSolverContext() { + try { + return (MemorySegment) createContext.invoke(); + } catch (Throwable t) { + throw rethrow(t); + } + } + + /** Destroy a solver context returned by {@link #createSolverContext()}. */ + public void destroySolverContext(MemorySegment ctx) { + invokeVoid(destroyContext, ctx); + } + + /** Solve a single board. Returns a RETURN_* status code. */ + public int solveBoard(MemorySegment ctx, MemorySegment deal, int target, + int solutions, int mode, MemorySegment futureTricks) { + try { + return (int) solveBoard.invoke(ctx, deal, target, solutions, mode, futureTricks); + } catch (Throwable t) { + throw rethrow(t); + } + } + + /** Compute the double dummy table for a deal. Returns a RETURN_* code. */ + public int calcDdTable(MemorySegment ctx, MemorySegment deal, MemorySegment results) { + try { + return (int) calcDdTable.invoke(ctx, deal, results); + } catch (Throwable t) { + throw rethrow(t); + } + } + + /** Compute the par result for a deal. Returns a RETURN_* code. */ + public int calcPar(MemorySegment ctx, MemorySegment deal, int vulnerable, + MemorySegment results, MemorySegment par) { + try { + return (int) calcPar.invoke(ctx, deal, vulnerable, results, par); + } catch (Throwable t) { + throw rethrow(t); + } + } + + @Override + public void close() { + arena.close(); + } + + private static void invokeVoid(MethodHandle handle, MemorySegment arg) { + try { + handle.invoke(arg); + } catch (Throwable t) { + throw rethrow(t); + } + } + + private static RuntimeException rethrow(Throwable t) { + if (t instanceof RuntimeException re) { + return re; + } + if (t instanceof Error err) { + // Propagate JVM-fatal conditions (OutOfMemoryError, LinkageError, + // ...) unchanged; wrapping them would hide them behind callers' + // RuntimeException handling. + throw err; + } + return new RuntimeException(t); + } +} diff --git a/jni/java/org/dds/ffm/DdsEmbeddedSmokeTest.java b/jni/java/org/dds/ffm/DdsEmbeddedSmokeTest.java new file mode 100644 index 00000000..7bdfc116 --- /dev/null +++ b/jni/java/org/dds/ffm/DdsEmbeddedSmokeTest.java @@ -0,0 +1,103 @@ +/* + DDS, a bridge double dummy solver. + + Smoke test for the packaged jar: load the native library via + Dds.loadEmbedded() (from the jar's classpath resource, not a filesystem + path) and solve a known deal, proving the embed -> extract -> load chain. + The broader shim surface is covered by DdsSmokeTest; this test only varies + how the library is located. + + Runs as a plain main (use_testrunner = False): success exits 0, any failure + throws and fails the test. + + See LICENSE and README. +*/ + +package org.dds.ffm; + +import static java.lang.foreign.ValueLayout.JAVA_INT; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout.PathElement; +import java.lang.foreign.MemorySegment; + +public final class DdsEmbeddedSmokeTest { + + private static final int FULL_SUIT = 0x7FFC; + private static final int RETURN_NO_FAULT = 1; + + private static final long DEAL_TRUMP = Dds.DEAL.byteOffset(PathElement.groupElement("trump")); + private static final long DEAL_REMAIN = Dds.DEAL.byteOffset(PathElement.groupElement("remainCards")); + private static final long FT_CARDS = Dds.FUTURE_TRICKS.byteOffset(PathElement.groupElement("cards")); + private static final long FT_SCORE = Dds.FUTURE_TRICKS.byteOffset(PathElement.groupElement("score")); + private static final long INFO_MAJOR = Dds.DDS_INFO.byteOffset(PathElement.groupElement("major")); + private static final long INFO_MINOR = Dds.DDS_INFO.byteOffset(PathElement.groupElement("minor")); + + public static void main(String[] args) { + try (Dds dds = Dds.loadEmbedded(); Arena arena = Arena.ofConfined()) { + checkCoordinateMatchesLibrary(dds, arena); + // North all spades (trump), East hearts, South diamonds, West clubs; + // North leads a trump every trick -> 13 tricks. Cross-checked in + // DdsSmokeTest. + MemorySegment deal = arena.allocate(Dds.DEAL); + // Arena.allocate is not guaranteed zero-initialized; clear the input + // struct so currentTrick* and untouched remainCards entries are 0. + deal.fill((byte) 0); + deal.set(JAVA_INT, DEAL_TRUMP, 0); + setRemain(deal, 0, 0, FULL_SUIT); + setRemain(deal, 1, 1, FULL_SUIT); + setRemain(deal, 2, 2, FULL_SUIT); + setRemain(deal, 3, 3, FULL_SUIT); + + MemorySegment ctx = dds.createSolverContext(); + try { + MemorySegment fut = arena.allocate(Dds.FUTURE_TRICKS); + int rc = dds.solveBoard(ctx, deal, -1, 1, 1, fut); + check(rc == RETURN_NO_FAULT, "dds_c_solve_board returned " + rc); + int cards = fut.get(JAVA_INT, FT_CARDS); + int topScore = fut.get(JAVA_INT, FT_SCORE); + System.out.println("embedded solve_board: cards=" + cards + " score[0]=" + topScore); + check(cards == 1, "expected 1 candidate card, got " + cards); + check(topScore == 13, "expected 13 tricks, got " + topScore); + } finally { + dds.destroySolverContext(ctx); + } + } + System.out.println("DDS embedded FFM smoke test passed."); + } + + private static void checkCoordinateMatchesLibrary(Dds dds, Arena arena) { + // Guard against the published Maven version silently diverging from the + // embedded native library: the coordinate's major.minor must match the + // library's GetDDSInfo. Patch may differ (package vs C-API versioning). + String coordinate = System.getProperty("dds.coordinate.version"); + if (coordinate == null || coordinate.isBlank()) { + return; // only enforced when the build passes the coordinate version + } + MemorySegment info = arena.allocate(Dds.DDS_INFO); + dds.getDdsInfo(info); + int libMajor = info.get(JAVA_INT, INFO_MAJOR); + int libMinor = info.get(JAVA_INT, INFO_MINOR); + String[] parts = coordinate.split("\\."); + int coordMajor = Integer.parseInt(parts[0]); + int coordMinor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; + System.out.println("version check: coordinate=" + coordinate + + " library=" + libMajor + "." + libMinor); + check(libMajor == coordMajor && libMinor == coordMinor, + "Maven coordinate " + coordinate + " major.minor does not match library " + + libMajor + "." + libMinor); + } + + private static void setRemain(MemorySegment deal, int hand, int suit, int holding) { + deal.set(JAVA_INT, DEAL_REMAIN + (long) (hand * 4 + suit) * Integer.BYTES, holding); + } + + private static void check(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + private DdsEmbeddedSmokeTest() { + } +} diff --git a/jni/java/org/dds/ffm/DdsSmokeTest.java b/jni/java/org/dds/ffm/DdsSmokeTest.java new file mode 100644 index 00000000..5e60d7a6 --- /dev/null +++ b/jni/java/org/dds/ffm/DdsSmokeTest.java @@ -0,0 +1,273 @@ +/* + DDS, a bridge double dummy solver. + + End-to-end FFM smoke test: load the native shared library, query GetDDSInfo, + then create a solver context and solve a known deal through the dds_c_* shim, + asserting the double dummy result. Mirrors python/tests/test_import.py / + python_interface_smoke_test. + + Run as a plain main (use_testrunner = False): success exits 0, any failure + throws and fails the test. + + See LICENSE and README. +*/ + +package org.dds.ffm; + +import static java.lang.foreign.ValueLayout.JAVA_BYTE; +import static java.lang.foreign.ValueLayout.JAVA_INT; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemoryLayout.PathElement; +import java.lang.foreign.MemorySegment; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public final class DdsSmokeTest { + + // Full 13-card holding bitmask (ranks 2..A), matching the Python fixtures. + private static final int FULL_SUIT = 0x7FFC; + private static final int RETURN_NO_FAULT = 1; + + // Double dummy table for the known deal, res_table[strain][hand] flattened + // (5 strains x 4 hands). Cross-checked against the Python binding. + private static final int[] EXPECTED_DD_TABLE = { + 13, 0, 13, 0, // spades + 0, 13, 0, 13, // hearts + 13, 0, 13, 0, // diamonds + 0, 13, 0, 13, // clubs + 0, 0, 0, 0, // no-trump + }; + + // Byte offsets derived once from the public layouts (robust to edits). + private static final long DEAL_TRUMP = Dds.DEAL.byteOffset(PathElement.groupElement("trump")); + private static final long DEAL_FIRST = Dds.DEAL.byteOffset(PathElement.groupElement("first")); + private static final long DEAL_REMAIN = Dds.DEAL.byteOffset(PathElement.groupElement("remainCards")); + private static final long FT_CARDS = Dds.FUTURE_TRICKS.byteOffset(PathElement.groupElement("cards")); + private static final long FT_SCORE = Dds.FUTURE_TRICKS.byteOffset(PathElement.groupElement("score")); + private static final long INFO_SYSTEM_STRING = + Dds.DDS_INFO.byteOffset(PathElement.groupElement("systemString")); + private static final long INFO_SYSTEM_STRING_LEN = + Dds.DDS_INFO.select(PathElement.groupElement("systemString")).byteSize(); + private static final long INFO_NO_OF_THREADS = + Dds.DDS_INFO.byteOffset(PathElement.groupElement("noOfThreads")); + private static final long DTD_CARDS = + Dds.DD_TABLE_DEAL.byteOffset(PathElement.groupElement("cards")); + private static final long DTR_RES_TABLE = + Dds.DD_TABLE_RESULTS.byteOffset(PathElement.groupElement("resTable")); + private static final long PAR_SCORE = + Dds.PAR_RESULTS.byteOffset(PathElement.groupElement("parScore")); + // parScore is char[2][16] (NS entry then EW); we read only the NS entry. + private static final long PAR_SCORE_NS_LEN = + Dds.PAR_RESULTS.select(PathElement.groupElement("parScore")).byteSize() / 2; + + public static void main(String[] args) throws Exception { + Path library = locateLibrary(); + System.out.println("Loading DDS shared library: " + library); + + try (Dds dds = Dds.load(library); Arena arena = Arena.ofConfined()) { + checkDdsInfo(dds, arena); + checkSolveKnownDeal(dds, arena); + checkSolveRejectsInvalidDeal(dds, arena); + checkCalcDdTable(dds, arena); + checkCalcPar(dds, arena); + } + System.out.println("DDS FFM smoke test passed."); + } + + private static void checkDdsInfo(Dds dds, Arena arena) { + MemorySegment info = arena.allocate(Dds.DDS_INFO); + dds.getDdsInfo(info); + + String systemString = readCString(info, INFO_SYSTEM_STRING, INFO_SYSTEM_STRING_LEN); + int threads = info.get(JAVA_INT, INFO_NO_OF_THREADS); + System.out.println("GetDDSInfo: threads=" + threads + " system=\"" + systemString + "\""); + check(!systemString.isBlank(), "GetDDSInfo systemString should be non-empty"); + check(threads >= 1, "GetDDSInfo noOfThreads should be >= 1, was " + threads); + } + + private static void checkSolveKnownDeal(Dds dds, Arena arena) { + // North holds all spades, East all hearts, South all diamonds, West all + // clubs; spades are trump and North leads. North leads a trump (spade) + // every trick and no other hand can follow, so North wins all 13 tricks + // (score[0]) taking the ace of spades (verified against the Python + // binding). + MemorySegment deal = arena.allocate(Dds.DEAL); + // Arena.allocate does not guarantee zeroed memory; clear the input + // struct so currentTrick* and untouched remainCards entries are 0. + deal.fill((byte) 0); + deal.set(JAVA_INT, DEAL_TRUMP, 0); // trump = spades + deal.set(JAVA_INT, DEAL_FIRST, 0); // first = North + setRemain(deal, 0, 0, FULL_SUIT); // North spades + setRemain(deal, 1, 1, FULL_SUIT); // East hearts + setRemain(deal, 2, 2, FULL_SUIT); // South diamonds + setRemain(deal, 3, 3, FULL_SUIT); // West clubs + + MemorySegment ctx = dds.createSolverContext(); + check(!ctx.equals(MemorySegment.NULL), "createSolverContext returned NULL"); + try { + MemorySegment fut = arena.allocate(Dds.FUTURE_TRICKS); + int rc = dds.solveBoard(ctx, deal, -1, 1, 1, fut); + check(rc == RETURN_NO_FAULT, "dds_c_solve_board returned " + rc); + + int cards = fut.get(JAVA_INT, FT_CARDS); + int topScore = fut.get(JAVA_INT, FT_SCORE); + System.out.println("solve_board: cards=" + cards + " score[0]=" + topScore); + check(cards == 1, "expected 1 candidate card, got " + cards); + check(topScore == 13, "expected 13 tricks, got " + topScore); + } finally { + dds.destroySolverContext(ctx); + } + } + + private static void checkSolveRejectsInvalidDeal(Dds dds, Arena arena) { + // Reuse the valid 52-card fixture and change only the trump: trump = 5 + // is out of range (valid 0..4), so the rejection is attributable to + // trump-range validation and not to an incomplete/invalid deal. The + // shim must surface the solver's error code rather than succeeding or + // crashing. + MemorySegment deal = arena.allocate(Dds.DEAL); + deal.fill((byte) 0); // allocate is not guaranteed zero-initialized + deal.set(JAVA_INT, DEAL_TRUMP, 5); // out of range (valid 0..4) + deal.set(JAVA_INT, DEAL_FIRST, 0); // first = North + setRemain(deal, 0, 0, FULL_SUIT); // North spades + setRemain(deal, 1, 1, FULL_SUIT); // East hearts + setRemain(deal, 2, 2, FULL_SUIT); // South diamonds + setRemain(deal, 3, 3, FULL_SUIT); // West clubs + + MemorySegment ctx = dds.createSolverContext(); + try { + MemorySegment fut = arena.allocate(Dds.FUTURE_TRICKS); + int rc = dds.solveBoard(ctx, deal, -1, 1, 1, fut); + System.out.println("solve_board(invalid trump): rc=" + rc); + check(rc != RETURN_NO_FAULT, "invalid deal should not return success, got " + rc); + } finally { + dds.destroySolverContext(ctx); + } + } + + private static void checkCalcDdTable(Dds dds, Arena arena) { + // Same holdings as the solve fixture; DdTableDeal.cards is [hand][suit]. + MemorySegment tableDeal = arena.allocate(Dds.DD_TABLE_DEAL); + // allocate is not guaranteed zero-initialized; clear the 12 unset cards. + tableDeal.fill((byte) 0); + setHolding(tableDeal, DTD_CARDS, 0, 0, FULL_SUIT); + setHolding(tableDeal, DTD_CARDS, 1, 1, FULL_SUIT); + setHolding(tableDeal, DTD_CARDS, 2, 2, FULL_SUIT); + setHolding(tableDeal, DTD_CARDS, 3, 3, FULL_SUIT); + + MemorySegment ctx = dds.createSolverContext(); + try { + MemorySegment results = arena.allocate(Dds.DD_TABLE_RESULTS); + int rc = dds.calcDdTable(ctx, tableDeal, results); + check(rc == RETURN_NO_FAULT, "dds_c_calc_dd_table returned " + rc); + + for (int i = 0; i < EXPECTED_DD_TABLE.length; i++) { + int got = results.get(JAVA_INT, DTR_RES_TABLE + (long) i * Integer.BYTES); + check(got == EXPECTED_DD_TABLE[i], + "resTable[" + i + "] expected " + EXPECTED_DD_TABLE[i] + ", got " + got); + } + System.out.println("calc_dd_table: 5x4 table matches expected."); + } finally { + dds.destroySolverContext(ctx); + } + } + + private static void checkCalcPar(Dds dds, Arena arena) { + MemorySegment tableDeal = arena.allocate(Dds.DD_TABLE_DEAL); + tableDeal.fill((byte) 0); // allocate is not guaranteed zero-initialized + setHolding(tableDeal, DTD_CARDS, 0, 0, FULL_SUIT); + setHolding(tableDeal, DTD_CARDS, 1, 1, FULL_SUIT); + setHolding(tableDeal, DTD_CARDS, 2, 2, FULL_SUIT); + setHolding(tableDeal, DTD_CARDS, 3, 3, FULL_SUIT); + + MemorySegment ctx = dds.createSolverContext(); + try { + MemorySegment results = arena.allocate(Dds.DD_TABLE_RESULTS); + MemorySegment par = arena.allocate(Dds.PAR_RESULTS); + int rc = dds.calcPar(ctx, tableDeal, 0 /* vulnerable: none */, results, par); + check(rc == RETURN_NO_FAULT, "dds_c_calc_par returned " + rc); + + String parScore = readCString(par, PAR_SCORE, PAR_SCORE_NS_LEN); + System.out.println("calc_par: NS par score = \"" + parScore + "\""); + check(!parScore.isBlank(), "calc_par NS par_score should be non-empty"); + } finally { + dds.destroySolverContext(ctx); + } + } + + private static void setRemain(MemorySegment deal, int hand, int suit, int holding) { + // remainCards[hand][suit], row-major with DDS_SUITS = 4 columns. + setHolding(deal, DEAL_REMAIN, hand, suit, holding); + } + + private static void setHolding(MemorySegment struct, long base, int hand, int suit, int holding) { + // [hand][suit] array, row-major with DDS_SUITS = 4 columns. + struct.set(JAVA_INT, base + (long) (hand * 4 + suit) * Integer.BYTES, holding); + } + + private static String readCString(MemorySegment struct, long offset, long maxLength) { + // Read a fixed-size char[] field as a NUL-terminated string. Bound the + // scan to the field length (clamped to the segment) so a missing + // terminator cannot run into an adjacent field and return a bogus + // concatenated string; instead it fails with a clear error. + long limit = Math.min(offset + maxLength, struct.byteSize()); + StringBuilder sb = new StringBuilder(); + for (long i = offset; i < limit; i++) { + byte b = struct.get(JAVA_BYTE, i); + if (b == 0) { + return sb.toString(); + } + sb.append((char) (b & 0xFF)); + } + throw new IllegalStateException( + "unterminated C string at offset " + offset + " within " + maxLength + " bytes"); + } + + private static Path locateLibrary() throws Exception { + String prop = System.getProperty("dds.library.path"); + if (prop != null && !prop.isBlank()) { + // An explicitly-set property is authoritative: use it or fail fast. + // Do not fall back to scanning, which could non-deterministically + // load a different libdds.* found elsewhere in the tree. + Path p = Path.of(prop); + if (Files.isRegularFile(p)) { + return p; + } + throw new IllegalStateException( + "dds.library.path does not point to a file: " + prop); + } + // No property set: fall back to searching the runfiles tree for the + // built artifact. + // Bound the walk depth so a missing/incorrect dds.library.path does not + // send us traversing an arbitrarily large checkout or runfiles tree. + String[] names = {"libdds.dylib", "libdds.so", "dds.dll"}; + Path root = Path.of(System.getProperty("user.dir")); + int maxDepth = 12; + try (Stream walk = Files.walk(root, maxDepth)) { + return walk.filter(Files::isRegularFile) + .filter(p -> { + String n = p.getFileName().toString(); + for (String name : names) { + if (n.equals(name)) { + return true; + } + } + return false; + }) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "DDS shared library not found (dds.library.path=" + prop + ")")); + } + } + + private static void check(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } + + private DdsSmokeTest() { + } +} diff --git a/jni/tests/BUILD.bazel b/jni/tests/BUILD.bazel new file mode 100644 index 00000000..f470b11b --- /dev/null +++ b/jni/tests/BUILD.bazel @@ -0,0 +1,45 @@ +load("@rules_python//python:defs.bzl", "py_test") + +# Verifies the shared library exports exactly the public C API (dll.h flat API +# plus the dds_c_* shim) and that no C++-mangled symbol leaks. The expected set +# is parsed from the headers (not the generated .lds) so a stale .lds is caught. +# Windows uses a different export mechanism (__declspec) and a different symbol +# dumper, so the nm-based check is scoped to Unix hosts. +py_test( + name = "export_set_test", + srcs = ["export_set_test.py"], + args = [ + "$(location //jni:dds_shared)", + "$(location //library/src/api:dll.h)", + "$(location //library/src/api:dds_c_api.h)", + ], + data = [ + "//jni:dds_shared", + "//library/src/api:dll.h", + "//library/src/api:dds_c_api.h", + ], + deps = ["//jni:gen_export_lists_lib"], + target_compatible_with = select({ + "//:build_windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) + +# Unit-tests the export-list generator's parser directly (header snippets in, +# symbol set out) — independent of the linked library. +py_test( + name = "gen_export_lists_test", + srcs = ["gen_export_lists_test.py"], + deps = ["//jni:gen_export_lists_lib"], +) + +# Inspects the built jar directly (not the Bazel classpath) to prove java_export +# bundled both the binding classes and the native library into the artifact a +# Maven consumer would actually receive. Inherits dds_ffm_dist's platform +# compatibility, so it is skipped on unsupported host os/cpu. +py_test( + name = "jar_self_contained_test", + srcs = ["jar_self_contained_test.py"], + args = ["$(location //jni:dds_ffm_dist)"], + data = ["//jni:dds_ffm_dist"], +) diff --git a/jni/tests/export_set_test.py b/jni/tests/export_set_test.py new file mode 100644 index 00000000..2847acce --- /dev/null +++ b/jni/tests/export_set_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Assert the shared library exports exactly the public C API and nothing else. + +The expected symbol set is derived directly from the public headers (dll.h, +dds_c_api.h) via the same parser that generates the export lists — NOT from the +generated .lds. This is deliberate: driving "expected" from the .lds would be +tautological (the .lds also drives what the linker exports), so a public symbol +added to a header but never regenerated into the .lds would go undetected. By +parsing the headers instead, a stale .lds surfaces as a missing export. + +This test: + * confirms every symbol declared in the headers is actually exported, and + * confirms no C++-mangled symbol (Itanium `_Z...`) leaks into the ABI. + +Invoked as: export_set_test.py
... +""" + +import re +import subprocess +import sys + +import gen_export_lists + + +def expected_symbols(header_paths): + """Public symbols declared in the headers (source of truth for the ABI).""" + return set(gen_export_lists.parse_symbols(header_paths)) + + +def exported_symbols(lib_path): + """Return the set of externally-defined text symbols in the library. + + Only `-g` (external symbols) is passed; defined-vs-undefined is decided from + the symbol-type column (uppercase `T` = external defined text), so we do not + depend on `-U`, whose meaning differs between BSD nm and llvm/GNU nm. + """ + if sys.platform == "darwin": + out = subprocess.check_output(["nm", "-g", lib_path], text=True) + strip_underscore = True + else: + out = subprocess.check_output(["nm", "-D", "-g", lib_path], text=True) + strip_underscore = False + + names = set() + for line in out.splitlines(): + parts = line.split() + if len(parts) < 2: + continue + sym_type, name = parts[-2], parts[-1] + if sym_type != "T": # external defined text symbols only ('U'/'t' excluded) + continue + if strip_underscore and name.startswith("_"): + name = name[1:] + names.add(name) + return names + + +def main(argv): + lib_path, header_paths = argv[1], argv[2:] + expected = expected_symbols(header_paths) + exported = exported_symbols(lib_path) + + missing = sorted(expected - exported) + if missing: + print("FAIL: expected public symbols not exported:", *missing, sep="\n ") + return 1 + + mangled = sorted(s for s in exported if re.match(r"^_?_Z", s)) + if mangled: + print("FAIL: C++-mangled symbols leaked into the ABI:", *mangled, sep="\n ") + return 1 + + extra = sorted(exported - expected) + if extra: + print("FAIL: unexpected symbols exported:", *extra, sep="\n ") + return 1 + + print(f"OK: {len(expected)} public symbols exported, no leaks.") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/jni/tests/gen_export_lists_test.py b/jni/tests/gen_export_lists_test.py new file mode 100644 index 00000000..ba740c23 --- /dev/null +++ b/jni/tests/gen_export_lists_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Unit tests for gen_export_lists.parse_symbols. + +Exercises the parser directly on header snippets — the actual logic worth +testing — rather than the linked library. Covers the flat trailing-return +`auto STDCALL Name(` form, the shim `Type name(` form, and the tokens the +parser must ignore (preprocessor #define lines and comments). +""" + +import os +import tempfile +import unittest + +import gen_export_lists as gel + + +def parse(*snippets): + paths = [] + try: + for text in snippets: + handle = tempfile.NamedTemporaryFile( + "w", suffix=".h", delete=False, encoding="utf-8" + ) + handle.write(text) + handle.close() + paths.append(handle.name) + return gel.parse_symbols(paths) + finally: + for path in paths: + os.unlink(path) + + +class ParseSymbolsTest(unittest.TestCase): + + def test_flat_trailing_return_form(self): + header = ( + "EXTERN_C DLLEXPORT auto STDCALL SolveBoard(\n" + " struct Deal dl) -> int;\n" + "EXTERN_C DLLEXPORT auto STDCALL GetDDSInfo(\n" + " struct DDSInfo * info) -> void;\n" + ) + self.assertEqual(parse(header), ["GetDDSInfo", "SolveBoard"]) + + def test_shim_pointer_form(self): + header = ( + "DLLEXPORT int dds_c_solve_board(void* ctx, const struct Deal* dl);\n" + "DLLEXPORT void dds_c_destroy_solvercontext(void* ctx);\n" + ) + self.assertEqual( + parse(header), + ["dds_c_destroy_solvercontext", "dds_c_solve_board"], + ) + + def test_ignores_defines_and_comments(self): + # The macro-defining lines and comments must not leak DLLEXPORT, + # STDCALL, __declspec or commented-out declarations into the export set. + header = ( + "#define DLLEXPORT __declspec(dllexport)\n" + "#define STDCALL __stdcall\n" + "// EXTERN_C DLLEXPORT auto STDCALL Commented(int) -> int;\n" + "EXTERN_C DLLEXPORT auto STDCALL RealOne(int) -> int;\n" + ) + self.assertEqual(parse(header), ["RealOne"]) + + def test_dedup_and_sorted_across_headers(self): + a = "DLLEXPORT int dds_c_calc_par(void* c);\n" + b = ( + "DLLEXPORT int dds_c_calc_par(void* c);\n" # duplicate across headers + "EXTERN_C DLLEXPORT auto STDCALL CalcPar(int) -> int;\n" + ) + self.assertEqual(parse(a, b), ["CalcPar", "dds_c_calc_par"]) + + def test_empty_when_no_exports(self): + self.assertEqual(parse("int internal_only(void);\n"), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/jni/tests/jar_self_contained_test.py b/jni/tests/jar_self_contained_test.py new file mode 100644 index 00000000..84ed9a81 --- /dev/null +++ b/jni/tests/jar_self_contained_test.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Assert the published jar is self-contained. + +The embedded smoke test loads via the Bazel runtime classpath, where the +dds_ffm classes are present through java_export's `exports` edge regardless of +whether they were bundled into the Maven artifact. This test instead inspects +the built jar file directly, so a regression where java_export stopped merging +the exported classes (leaving a jar with only the native resource) is caught — +a real Maven consumer depending on the coordinate would otherwise hit +NoClassDefFoundError for org.dds.ffm.Dds. + +Invoked as: jar_self_contained_test.py +""" + +import re +import sys +import zipfile + +_NATIVE_LIB = re.compile(r"^native/[^/]+/(lib)?dds\.(so|dylib|dll)$") + + +def main(argv): + jar_path = argv[1] + with zipfile.ZipFile(jar_path) as jar: + names = jar.namelist() + + if "org/dds/ffm/Dds.class" not in names: + print("FAIL: org/dds/ffm/Dds.class not bundled in", jar_path) + return 1 + + native = [n for n in names if _NATIVE_LIB.match(n)] + if not native: + print("FAIL: no embedded native library (native//lib) in", jar_path) + return 1 + + print(f"OK: jar is self-contained (Dds.class + {native[0]}).") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/jni/version_script.lds b/jni/version_script.lds new file mode 100644 index 00000000..d02f1be6 --- /dev/null +++ b/jni/version_script.lds @@ -0,0 +1,49 @@ +{ + global: + AnalyseAllPlaysBin; + AnalyseAllPlaysPBN; + AnalysePlayBin; + AnalysePlayPBN; + CalcAllTables; + CalcAllTablesN; + CalcAllTablesPBN; + CalcAllTablesPBNN; + CalcDDtable; + CalcDDtableN; + CalcDDtablePBN; + CalcDDtablePBNN; + CalcPar; + CalcParPBN; + ConvertToDealerTextFormat; + ConvertToSidesTextFormat; + DealerPar; + DealerParBin; + ErrorMessage; + FreeMemory; + GetDDSInfo; + InitializeStaticMemory; + Par; + SetMaxThreads; + SetResources; + SetThreading; + SidesPar; + SidesParBin; + SolveAllBoards; + SolveAllBoardsBin; + SolveAllBoardsBinN; + SolveAllBoardsBinSeq; + SolveAllBoardsN; + SolveAllBoardsSeq; + SolveAllChunks; + SolveAllChunksBin; + SolveAllChunksPBN; + SolveBoard; + SolveBoardPBN; + dds_c_calc_dd_table; + dds_c_calc_par; + dds_c_create_solvercontext_default; + dds_c_destroy_solvercontext; + dds_c_solve_board; + local: + *; +}; diff --git a/library/src/api/BUILD.bazel b/library/src/api/BUILD.bazel index b3aacc6b..80910466 100644 --- a/library/src/api/BUILD.bazel +++ b/library/src/api/BUILD.bazel @@ -1,6 +1,13 @@ load("//:CPPVARIABLES.bzl", "DDS_CPPOPTS", "DDS_LINKOPTS", "DDS_LOCAL_DEFINES") load("@rules_cc//cc:defs.bzl", "cc_library") +# Public C-ABI headers, exposed as files so tooling (e.g. the export-set test and +# gen_export_lists.py) can parse them for the exported symbol set. +exports_files([ + "dll.h", + "dds_c_api.h", +]) + cc_library( name = "api_definitions", hdrs = [ @@ -8,7 +15,8 @@ cc_library( "calc_par.hpp", "dds.h", "dll.h", - "dds_api.hpp", + "dds_api.hpp", + "dds_c_api.h", "portab.h", "PBN.h", # Case-sensitive on consumers; cannot add pbn.h on macOS CI "solve_board.hpp", @@ -19,3 +27,20 @@ cc_library( "//library/src/utility:constants", ], ) + +# Pure-C ABI shim over the modern SolverContext API. Pointer-only, POD-only, +# opaque void* handle — the stable surface Java (FFM), .NET, and ctypes bind to. +cc_library( + name = "dds_c_api", + srcs = ["dds_c_api.cpp"], + hdrs = ["dds_c_api.h"], + include_prefix = "api", + copts = DDS_CPPOPTS, + linkopts = DDS_LINKOPTS, + local_defines = DDS_LOCAL_DEFINES, + visibility = ["//visibility:public"], + deps = [ + ":api_definitions", + "//library/src:dds", + ], +) diff --git a/library/src/api/dds_c_api.cpp b/library/src/api/dds_c_api.cpp new file mode 100644 index 00000000..a66499e6 --- /dev/null +++ b/library/src/api/dds_c_api.cpp @@ -0,0 +1,95 @@ +/* + DDS, a bridge double dummy solver. + + Implementation of the pure-C ABI shim. Each function casts the opaque + void* handle back to SolverContext* and forwards to the reference-taking + dds_* functions declared in dds_api.hpp. SolverConfig / TTKind stay internal + to this translation unit and never cross the shim boundary. + + See LICENSE and README. +*/ + +#include +#include + +/* This shim is the boundary between the C++ implementation and pure-C FFI + consumers (JVM/FFM, .NET, ctypes). Two things must never cross it: + + - C++ exceptions: unwinding through a C ABI frame is undefined behavior, + so every entry point wraps its body in catch-all handlers. + - Undefined behavior from NULL handles/arguments: the reference-taking + dds_* functions dereference their inputs, so we validate pointers up + front and fail fast with RETURN_UNKNOWN_FAULT instead of crashing the + caller's process. */ + +extern "C" { + +DLLEXPORT DDS_C_SOLVER_CTX dds_c_create_solvercontext_default(void) +{ + try { + return static_cast(dds_create_solvercontext_default()); + } catch (...) { + return nullptr; + } +} + +DLLEXPORT void dds_c_destroy_solvercontext(DDS_C_SOLVER_CTX ctx) +{ + if (ctx == nullptr) + return; + + try { + dds_destroy_solvercontext(static_cast(ctx)); + } catch (...) { + // A destructor must not let an exception escape the C ABI boundary. + } +} + +DLLEXPORT int dds_c_solve_board(DDS_C_SOLVER_CTX ctx, + const struct Deal* dl, + int target, int solutions, int mode, + struct FutureTricks* futp) +{ + if (ctx == nullptr || dl == nullptr || futp == nullptr) + return RETURN_UNKNOWN_FAULT; + + try { + return dds_solve_board(static_cast(ctx), + *dl, target, solutions, mode, futp); + } catch (...) { + return RETURN_UNKNOWN_FAULT; + } +} + +DLLEXPORT int dds_c_calc_dd_table(DDS_C_SOLVER_CTX ctx, + const struct DdTableDeal* deal, + struct DdTableResults* results) +{ + if (ctx == nullptr || deal == nullptr || results == nullptr) + return RETURN_UNKNOWN_FAULT; + + try { + return dds_calc_dd_table(static_cast(ctx), *deal, results); + } catch (...) { + return RETURN_UNKNOWN_FAULT; + } +} + +DLLEXPORT int dds_c_calc_par(DDS_C_SOLVER_CTX ctx, + const struct DdTableDeal* deal, + int vulnerable, + struct DdTableResults* results, + struct ParResults* par) +{ + if (ctx == nullptr || deal == nullptr || results == nullptr || par == nullptr) + return RETURN_UNKNOWN_FAULT; + + try { + return dds_calc_par(static_cast(ctx), + *deal, vulnerable, results, par); + } catch (...) { + return RETURN_UNKNOWN_FAULT; + } +} + +} // extern "C" diff --git a/library/src/api/dds_c_api.h b/library/src/api/dds_c_api.h new file mode 100644 index 00000000..975b1024 --- /dev/null +++ b/library/src/api/dds_c_api.h @@ -0,0 +1,59 @@ +/* + DDS, a bridge double dummy solver. + + C-ABI shim over the modern SolverContext API (dds_api.hpp). + + Every function here is C-callable and takes only pointers and plain-old-data + by value: the opaque handle is a void*, and non-POD C++ types (SolverConfig, + TTKind, C++ references) never appear. This gives Java (FFM/jextract), .NET, + and ctypes a single clean, stable ABI to bind against. + + NOTE: the *exported symbols* are a pure C ABI, but this header is not itself + compilable by a C front-end: it includes , whose flat API is + declared with C++ trailing-return syntax (auto ... -> int). Consume the ABI + by binding to the compiled library's symbols (FFM/ctypes/.NET) or by parsing + the headers with a C++ mode (jextract); do not #include this from a C + translation unit. + + See LICENSE and README. +*/ + +#pragma once + +#include /* struct Deal, FutureTricks, DdTableDeal, DdTableResults, ParResults */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Opaque solver-context handle. Owns per-context solver state and the + transposition table. Create with dds_c_create_solvercontext_default and + release with dds_c_destroy_solvercontext. Not thread-safe: use one handle + per thread. */ +typedef void* DDS_C_SOLVER_CTX; + +/* Creation / destruction. */ +DLLEXPORT DDS_C_SOLVER_CTX dds_c_create_solvercontext_default(void); +DLLEXPORT void dds_c_destroy_solvercontext(DDS_C_SOLVER_CTX ctx); + +/* Solve a single board. Returns a RETURN_* status code. */ +DLLEXPORT int dds_c_solve_board(DDS_C_SOLVER_CTX ctx, + const struct Deal* dl, + int target, int solutions, int mode, + struct FutureTricks* futp); + +/* Compute the double dummy table for a deal. */ +DLLEXPORT int dds_c_calc_dd_table(DDS_C_SOLVER_CTX ctx, + const struct DdTableDeal* deal, + struct DdTableResults* results); + +/* Compute the par result for a deal (computes the DD table internally). */ +DLLEXPORT int dds_c_calc_par(DDS_C_SOLVER_CTX ctx, + const struct DdTableDeal* deal, + int vulnerable, + struct DdTableResults* results, + struct ParResults* par); + +#ifdef __cplusplus +} +#endif