Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions docs/jni_interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ and ctypes can all bind to:

The shim covers the modern API's full surface: context lifecycle (default and
config-based creation, destroy), `dds_c_solve_board`, `dds_c_calc_dd_table` and
its `_pbn` twin, `dds_c_calc_par`, transposition-table configure/resize/clear,
both resets, and the logging passthroughs. The Java bindings here use a subset;
the .NET binding uses all of it ([dotnet_interface.md](dotnet_interface.md)).
its `_pbn` twin, `dds_c_calc_par`, `dds_c_convert_from_pbn`,
transposition-table configure/resize/clear, both resets, and the logging
passthroughs. The Java bindings here use a subset; the .NET binding uses all of
it except `dds_c_convert_from_pbn`
([dotnet_interface.md](dotnet_interface.md)).
The flat legacy C API from `dll.h` (`SolveBoard`, `CalcDDtable`, `GetDDSInfo`,
`ErrorMessage`, …) is exported unchanged and is also callable from FFM.

Expand All @@ -93,8 +95,8 @@ non-hermetic early-access binaries, and the generated output is just plain
exposes:

- Public `MemoryLayout` constants matching the C structs: `DEAL`,
`FUTURE_TRICKS`, `DD_TABLE_DEAL`, `DD_TABLE_RESULTS`, `PAR_RESULTS`,
`DDS_INFO`.
`FUTURE_TRICKS`, `DD_TABLE_DEAL`, `DD_TABLE_DEAL_PBN`, `DD_TABLE_RESULTS`,
`PAR_RESULTS`, `DDS_INFO`.
- Typed downcall wrappers for the shim functions plus `getDdsInfo`. These
return an `int` [`DdsStatus`](../jni/java/org/dds/ffm/DdsStatus.java)
`RETURN_*` code (`RETURN_NO_FAULT == 1` on success); compare against the named
Expand Down
3 changes: 1 addition & 2 deletions jni/exported_symbols.lds
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ _SolveBoardPBN
_dds_c_calc_dd_table
_dds_c_calc_dd_table_pbn
_dds_c_calc_par
_dds_c_calc_par_pbn
_dds_c_clear_tt
_dds_c_configure_tt
_dds_c_convert_from_pbn
_dds_c_convert_to_dealer_text_format
_dds_c_convert_to_sides_text_format
_dds_c_create_solvercontext
Expand All @@ -63,4 +63,3 @@ _dds_c_resize_tt
_dds_c_sides_par
_dds_c_sides_par_bin
_dds_c_solve_board
_dds_c_solve_board_pbn
50 changes: 50 additions & 0 deletions jni/java/org/dds/ffm/Dds.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public class Dds implements AutoCloseable {
MemoryLayout.sequenceLayout(16, JAVA_INT).withName("cards"))
.withName("DdTableDeal");

/** struct DdTableDealPBN — cards is a NUL-terminated PBN deal string. */
public static final MemoryLayout DD_TABLE_DEAL_PBN = MemoryLayout.structLayout(
MemoryLayout.sequenceLayout(80, JAVA_BYTE).withName("cards"))
.withName("DdTableDealPBN");

/** 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"))
Expand Down Expand Up @@ -114,6 +119,8 @@ public class Dds implements AutoCloseable {
private final MethodHandle solveBoard;
private final MethodHandle calcDdTable;
private final MethodHandle calcPar;
private final MethodHandle calcDdTablePbn;
private final MethodHandle convertFromPbn;

private Dds(Arena arena, SymbolLookup lookup) {
this.arena = arena;
Expand All @@ -130,6 +137,10 @@ private Dds(Arena arena, SymbolLookup lookup) {
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));
this.calcDdTablePbn = handle(linker, lookup, "dds_c_calc_dd_table_pbn",
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
this.convertFromPbn = handle(linker, lookup, "dds_c_convert_from_pbn",
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
}

private static MethodHandle handle(Linker linker, SymbolLookup lookup, String name,
Expand Down Expand Up @@ -281,6 +292,45 @@ public int calcPar(MemorySegment ctx, MemorySegment deal, int vulnerable,
}
}

/** Compute the double dummy table for a PBN-format deal. Returns a {@link DdsStatus} {@code RETURN_*} code. */
public int calcDdTablePbn(MemorySegment ctx, MemorySegment dealPbn, MemorySegment results) {
try {
return (int) calcDdTablePbn.invoke(ctx, dealPbn, results);
} catch (Throwable t) {
throw rethrow(t);
}
}

/**
* Parse a NUL-terminated PBN deal string into a binary holdings block: 16
* consecutive ints, row-major {@code [hand][suit]}, as laid out by
* {@link #DEAL}'s {@code remainCards} and {@link #DD_TABLE_DEAL}'s
* {@code cards}. Point {@code cards} at either field (or a slice of it) and
* then use the binary entry points; this is the general PBN path, which is
* why solve and par have no PBN variant here. The string must be shorter
* than 80 bytes including its terminator. Needs no solver context.
* Returns a {@link DdsStatus} {@code RETURN_*} code.
*
* <p>{@code RETURN_NO_FAULT} means the string parsed, not that it describes
* a legal deal: an unrecognized rank is skipped rather than refused, so a
* typo'd rank yields a short hand and still converts — except a compass
* letter (N/E/S/W, either case) found after the first hand, which IS
* refused with {@code RETURN_PBN_FAULT} rather than skipped. The solve and
* table calls validate the deal and return {@code RETURN_CARD_COUNT} or
* {@code RETURN_DUPLICATE_CARDS}, so check their status too.
*
* <p>On failure {@code cards} is not preserved — it is zeroed before parsing
* begins and may hold a partial parse. Convert into scratch storage if the
* destination must survive a bad string.
*/
public int convertFromPbn(MemorySegment pbnDeal, MemorySegment cards) {
try {
return (int) convertFromPbn.invoke(pbnDeal, cards);
} catch (Throwable t) {
throw rethrow(t);
}
}

@Override
public void close() {
arena.close();
Expand Down
91 changes: 91 additions & 0 deletions jni/java/org/dds/ffm/DdsSmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ public final class DdsSmokeTest {
// 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;
private static final long DEAL_REMAIN_LEN =
Dds.DEAL.select(PathElement.groupElement("remainCards")).byteSize();
private static final long DTDP_CARDS =
Dds.DD_TABLE_DEAL_PBN.byteOffset(PathElement.groupElement("cards"));
private static final long DTDP_CARDS_LEN =
Dds.DD_TABLE_DEAL_PBN.select(PathElement.groupElement("cards")).byteSize();

// Same reference board as checkSolveKnownDeal/checkCalcDdTable, in PBN
// format. Matches kReferencePbn in library/tests/dds_c_api_test.cpp so the
// JVM and C++ bindings agree on one fixture.
private static final String REFERENCE_PBN =
"N:AKQJT98765432... .AKQJT98765432.. ..AKQJT98765432. ...AKQJT98765432";

public static void main(String[] args) throws Exception {
Path library = locateLibrary();
Expand All @@ -74,6 +86,9 @@ public static void main(String[] args) throws Exception {
checkSolveRejectsInvalidDeal(dds, arena);
checkCalcDdTable(dds, arena);
checkCalcPar(dds, arena);
checkCalcDdTablePbn(dds, arena);
checkConvertFromPbn(dds, arena);
checkConvertFromPbnRejectsMalformedString(dds, arena);
}
System.out.println("DDS FFM smoke test passed.");
}
Expand Down Expand Up @@ -200,6 +215,68 @@ private static void checkCalcPar(Dds dds, Arena arena) {
}
}

private static void checkCalcDdTablePbn(Dds dds, Arena arena) {
MemorySegment tableDealPbn = arena.allocate(Dds.DD_TABLE_DEAL_PBN);
tableDealPbn.fill((byte) 0);
writeCString(tableDealPbn, DTDP_CARDS, DTDP_CARDS_LEN, REFERENCE_PBN);

MemorySegment ctx = dds.createSolverContext();
try {
MemorySegment results = arena.allocate(Dds.DD_TABLE_RESULTS);
int rc = dds.calcDdTablePbn(ctx, tableDealPbn, results);
check(rc == RETURN_NO_FAULT, "dds_c_calc_dd_table_pbn 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],
"pbn resTable[" + i + "] expected " + EXPECTED_DD_TABLE[i] + ", got " + got);
}
System.out.println("calc_dd_table_pbn: 5x4 table matches expected.");
} finally {
dds.destroySolverContext(ctx);
}
}

private static void checkConvertFromPbn(Dds dds, Arena arena) {
// The general PBN path for bindings, and the replacement for the
// withdrawn dds_c_solve_board_pbn: parse the PBN string straight into a
// binary Deal's remainCards, set the already-binary fields, then use the
// binary solve. Must reproduce checkSolveKnownDeal's 13 tricks.
MemorySegment deal = arena.allocate(Dds.DEAL);
deal.fill((byte) 0);
deal.set(JAVA_INT, DEAL_TRUMP, 0); // trump = spades
deal.set(JAVA_INT, DEAL_FIRST, 0); // first = North

MemorySegment pbn = arena.allocateFrom(REFERENCE_PBN);
int convertRc = dds.convertFromPbn(pbn, deal.asSlice(DEAL_REMAIN, DEAL_REMAIN_LEN));
check(convertRc == RETURN_NO_FAULT, "dds_c_convert_from_pbn returned " + convertRc);

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, "solve_board on converted deal returned " + rc);

int topScore = fut.get(JAVA_INT, FT_SCORE);
System.out.println("convert_from_pbn + solve_board: score[0]=" + topScore);
check(topScore == 13, "expected 13 tricks, got " + topScore);
} finally {
dds.destroySolverContext(ctx);
}
}

private static void checkConvertFromPbnRejectsMalformedString(Dds dds, Arena arena) {
MemorySegment cards = arena.allocate(Dds.DD_TABLE_DEAL);
MemorySegment pbn = arena.allocateFrom("xx");
int rc = dds.convertFromPbn(pbn, cards);
System.out.println("convert_from_pbn(malformed): rc=" + rc + " (" + DdsStatus.name(rc) + ")");
// Pin the mapped code, not just "not success": turning the parser's bare
// 0 into RETURN_PBN_FAULT is the shim's only added behavior here.
check(rc == DdsStatus.RETURN_PBN_FAULT,
"expected RETURN_PBN_FAULT, got " + DdsStatus.name(rc));
}

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);
Expand All @@ -210,6 +287,20 @@ private static void setHolding(MemorySegment struct, long base, int hand, int su
struct.set(JAVA_INT, base + (long) (hand * 4 + suit) * Integer.BYTES, holding);
}

private static void writeCString(MemorySegment struct, long offset, long fieldLength, String value) {
// Write value as ASCII into a fixed-size char[] field, zero-padding the
// remainder so it stays NUL-terminated for readCString/native use.
byte[] bytes = value.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
if (bytes.length >= fieldLength) {
throw new IllegalArgumentException(
"value does not fit in " + fieldLength + "-byte field (with NUL terminator): " + value);
}
for (long i = 0; i < fieldLength; i++) {
byte b = i < bytes.length ? bytes[(int) i] : 0;
struct.set(JAVA_BYTE, offset + i, b);
}
}

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
Expand Down
3 changes: 1 addition & 2 deletions jni/version_script.lds
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@
dds_c_calc_dd_table;
dds_c_calc_dd_table_pbn;
dds_c_calc_par;
dds_c_calc_par_pbn;
dds_c_clear_tt;
dds_c_configure_tt;
dds_c_convert_from_pbn;
dds_c_convert_to_dealer_text_format;
dds_c_convert_to_sides_text_format;
dds_c_create_solvercontext;
Expand All @@ -65,7 +65,6 @@
dds_c_sides_par;
dds_c_sides_par_bin;
dds_c_solve_board;
dds_c_solve_board_pbn;
local:
*;
};
16 changes: 1 addition & 15 deletions library/src/api/dds_api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,6 @@ extern "C" {
int mode,
FutureTricks* futp) -> int;

EXTERN_C DLLEXPORT auto dds_solve_board_pbn(DDS_SOLVER_CTX ctx,
const DealPBN& dlpbn,
int target,
int solutions,
int mode,
FutureTricks* futp) -> int;

EXTERN_C DLLEXPORT auto dds_calc_dd_table(
DDS_SOLVER_CTX ctx,
const DdTableDeal& table_deal,
Expand All @@ -71,12 +64,5 @@ extern "C" {
DdTableResults* table_results,
ParResults* par_results) -> int;

EXTERN_C DLLEXPORT auto dds_calc_par_pbn(
DDS_SOLVER_CTX ctx,
const DdTableDealPBN& table_deal_pbn,
int vulnerable,
DdTableResults* table_results,
ParResults* par_results) -> int;


}
}
53 changes: 20 additions & 33 deletions library/src/api/dds_c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <api/dds_c_api.h>
#include <api/dds_api.hpp>
#include <api/dll.h> /* legacy Par/SidesPar/DealerPar/.../GetDDSInfo/ErrorMessage */
#include <api/PBN.h> /* convert_from_pbn */

/* This shim is the boundary between the C++ implementation and pure-C FFI
consumers (JVM/FFM, .NET, ctypes). Two things must never cross it:
Expand Down Expand Up @@ -69,22 +70,6 @@ DLLEXPORT int dds_c_solve_board(DDS_C_SOLVER_CTX ctx,
}
}

DLLEXPORT int dds_c_solve_board_pbn(DDS_C_SOLVER_CTX ctx,
const struct DealPBN* dlpbn,
int target, int solutions, int mode,
struct FutureTricks* futp)
{
if (ctx == nullptr || dlpbn == nullptr || futp == nullptr)
return RETURN_UNKNOWN_FAULT;

try {
return dds_solve_board_pbn(static_cast<SolverContext*>(ctx),
*dlpbn, 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)
Expand Down Expand Up @@ -116,23 +101,6 @@ DLLEXPORT int dds_c_calc_par(DDS_C_SOLVER_CTX ctx,
}
}

DLLEXPORT int dds_c_calc_par_pbn(DDS_C_SOLVER_CTX ctx,
const struct DdTableDealPBN* 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_pbn(static_cast<SolverContext*>(ctx),
*deal, vulnerable, results, par);
} catch (...) {
return RETURN_UNKNOWN_FAULT;
}
}

DLLEXPORT DDS_C_SOLVER_CTX dds_c_create_solvercontext(int tt_kind,
int def_mb, int max_mb)
{
Expand Down Expand Up @@ -162,6 +130,25 @@ DLLEXPORT int dds_c_calc_dd_table_pbn(DDS_C_SOLVER_CTX ctx,
}
}

DLLEXPORT int dds_c_convert_from_pbn(const char* pbn_deal,
unsigned int cards[DDS_HANDS][DDS_SUITS])
{
if (pbn_deal == nullptr || cards == nullptr)
return RETURN_UNKNOWN_FAULT;

try {
/* convert_from_pbn reports success as RETURN_NO_FAULT but failure as a
bare 0, which is not a RETURN_* code; map it to RETURN_PBN_FAULT so
every status this shim returns is one. */
if (convert_from_pbn(pbn_deal, cards) != RETURN_NO_FAULT)
return RETURN_PBN_FAULT;
Comment thread
zzcgumn marked this conversation as resolved.

return RETURN_NO_FAULT;
} catch (...) {
return RETURN_UNKNOWN_FAULT;
}
}

DLLEXPORT void dds_c_configure_tt(DDS_C_SOLVER_CTX ctx, int tt_kind,
int def_mb, int max_mb)
{
Expand Down
Loading
Loading