Skip to content

Commit 720243e

Browse files
dfa1claude
andauthored
refactor: extract native-call helpers into NativeCall (#14)
* refactor: extract native-call helpers into NativeCall Move the FFM-downcall conventions out of Zstd into a dedicated package-private NativeCall: - interface ZstdCall (was SizeCall) - checkReturnValue(ZstdCall) (was call) — run a size_t call, decode a ZSTD_isError code into a ZstdException - isError / errorCode / errorName - requireNative segment guard Zstd keeps its one-shot byte[] API plus copyIn/copyOut. All call sites (contexts, streams, dicts, frame, bounds) repointed. No behaviour change; these are all package-private internals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: collapse 13 copies of the sneaky-rethrow helper into NativeCall Every binding class carried its own private rethrow/sneaky to launder the checked Throwable from MethodHandle.invokeExact. Replace them with one shared NativeCall.rethrow and repoint all catch blocks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: read ZSTD_bounds via the named layout, not magic offsets Name BOUNDS_LAYOUT's fields (error/lowerBound/upperBound) and derive the read offsets from it with byteOffset(groupElement(...)), so the struct reads track the definition instead of hand-counted 0/8/12. Document how the struct-by-value return allocates through the arena SegmentAllocator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 49e01b4 commit 720243e

15 files changed

Lines changed: 190 additions & 215 deletions

zstd/src/main/java/io/github/dfa1/zstd/Bindings.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,11 @@ final class Bindings {
158158
NativeLibrary.lookup("ZSTD_getDictID_fromDDict", FunctionDescriptor.of(JAVA_INT, ADDRESS));
159159

160160
// ZSTD_bounds { size_t error; int lowerBound; int upperBound; } — returned by value
161-
private static final MemoryLayout BOUNDS_LAYOUT =
162-
MemoryLayout.structLayout(JAVA_LONG, JAVA_INT, JAVA_INT);
161+
static final MemoryLayout BOUNDS_LAYOUT =
162+
MemoryLayout.structLayout(
163+
JAVA_LONG.withName("error"),
164+
JAVA_INT.withName("lowerBound"),
165+
JAVA_INT.withName("upperBound"));
163166
// ZSTD_bounds ZSTD_cParam_getBounds(ZSTD_cParameter) / ZSTD_dParam_getBounds(ZSTD_dParameter)
164167
static final MethodHandle CPARAM_GET_BOUNDS =
165168
NativeLibrary.lookup("ZSTD_cParam_getBounds", FunctionDescriptor.of(BOUNDS_LAYOUT, JAVA_INT));
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package io.github.dfa1.zstd;
2+
3+
import java.lang.foreign.MemorySegment;
4+
import java.nio.charset.StandardCharsets;
5+
6+
/// Package-private helpers that adapt raw FFM downcalls to the zstd error
7+
/// convention: run a native call, decode a zstd `size_t` error code into a
8+
/// {@link ZstdException}, and guard native-segment arguments. Shared by the
9+
/// binding classes so the conventions live in one place.
10+
final class NativeCall {
11+
12+
/// A native call returning a zstd `size_t` status that may encode an error.
13+
@FunctionalInterface
14+
interface ZstdCall {
15+
long run() throws Throwable;
16+
}
17+
18+
/// Invokes a size-returning zstd call and converts a zstd error code into a
19+
/// {@link ZstdException}.
20+
static long checkReturnValue(ZstdCall c) {
21+
long code;
22+
try {
23+
code = c.run();
24+
} catch (Throwable t) {
25+
throw rethrow(t);
26+
}
27+
if (isError(code)) {
28+
throw new ZstdException(errorName(code), ZstdErrorCode.of(errorCode(code)));
29+
}
30+
return code;
31+
}
32+
33+
static boolean isError(long code) {
34+
try {
35+
return ((int) Bindings.IS_ERROR.invokeExact(code)) != 0;
36+
} catch (Throwable t) {
37+
throw rethrow(t);
38+
}
39+
}
40+
41+
private static int errorCode(long code) {
42+
try {
43+
return (int) Bindings.GET_ERROR_CODE.invokeExact(code);
44+
} catch (Throwable t) {
45+
throw rethrow(t);
46+
}
47+
}
48+
49+
@SuppressWarnings("restricted") // reinterpret needed to read a C string of unknown length
50+
private static String errorName(long code) {
51+
try {
52+
MemorySegment p = (MemorySegment) Bindings.GET_ERROR_NAME.invokeExact(code);
53+
return p.reinterpret(Long.MAX_VALUE).getString(0, StandardCharsets.US_ASCII);
54+
} catch (Throwable t) {
55+
throw rethrow(t);
56+
}
57+
}
58+
59+
/// Guards a zero-copy entry point: the segment handed to zstd must be backed
60+
/// by native (off-heap) memory, since its address is dereferenced in C. Fails
61+
/// fast with a clear message instead of the FFM linker's cryptic error.
62+
static MemorySegment requireNative(MemorySegment seg, String name) {
63+
if (!seg.isNative()) {
64+
throw new IllegalArgumentException(
65+
name + " must be a native (off-heap) MemorySegment; got a heap segment");
66+
}
67+
return seg;
68+
}
69+
70+
/// Rethrows any `Throwable` as if unchecked, laundering the checked
71+
/// `Throwable` that {@link java.lang.invoke.MethodHandle#invokeExact} declares.
72+
/// The shared sink for every binding class's native-call catch blocks.
73+
@SuppressWarnings("unchecked")
74+
static <E extends Throwable> RuntimeException rethrow(Throwable t) throws E {
75+
throw (E) t;
76+
}
77+
78+
private NativeCall() {
79+
// no instances
80+
}
81+
}

zstd/src/main/java/io/github/dfa1/zstd/Zstd.java

Lines changed: 15 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static byte[] compress(byte[] src, int level) {
4242
MemorySegment in = copyIn(arena, src);
4343
long bound = compressBound(src.length);
4444
MemorySegment out = arena.allocate(bound);
45-
long written = call(() -> (long) Bindings.COMPRESS.invokeExact(
45+
long written = NativeCall.checkReturnValue(() -> (long) Bindings.COMPRESS.invokeExact(
4646
out, bound, in, (long) src.length, level));
4747
return copyOut(out, written);
4848
}
@@ -77,7 +77,7 @@ public static byte[] decompress(byte[] compressed, int maxSize) {
7777
try (Arena arena = Arena.ofConfined()) {
7878
MemorySegment in = copyIn(arena, compressed);
7979
MemorySegment out = arena.allocate(Math.max(maxSize, 1));
80-
long written = call(() -> (long) Bindings.DECOMPRESS.invokeExact(
80+
long written = NativeCall.checkReturnValue(() -> (long) Bindings.DECOMPRESS.invokeExact(
8181
out, (long) maxSize, in, (long) compressed.length));
8282
return copyOut(out, written);
8383
}
@@ -91,12 +91,12 @@ public static byte[] decompress(byte[] compressed, int maxSize) {
9191
/// @return the decompressed length in bytes
9292
/// @throws ZstdException if the frame is invalid or does not store its size
9393
public static long decompressedSize(MemorySegment frame) {
94-
requireNative(frame, "frame");
94+
NativeCall.requireNative(frame, "frame");
9595
long size;
9696
try {
9797
size = (long) Bindings.GET_FRAME_CONTENT_SIZE.invokeExact(frame, frame.byteSize());
9898
} catch (Throwable t) {
99-
throw sneaky(t);
99+
throw NativeCall.rethrow(t);
100100
}
101101
if (size == CONTENTSIZE_UNKNOWN) {
102102
throw new ZstdException("decompressed size not stored in frame");
@@ -116,7 +116,7 @@ public static long compressBound(long srcSize) {
116116
try {
117117
return (long) Bindings.COMPRESS_BOUND.invokeExact(srcSize);
118118
} catch (Throwable t) {
119-
throw sneaky(t);
119+
throw NativeCall.rethrow(t);
120120
}
121121
}
122122

@@ -126,7 +126,7 @@ private static long frameContentSize(byte[] compressed) {
126126
MemorySegment in = copyIn(arena, compressed);
127127
return (long) Bindings.GET_FRAME_CONTENT_SIZE.invokeExact(in, (long) compressed.length);
128128
} catch (Throwable t) {
129-
throw sneaky(t);
129+
throw NativeCall.rethrow(t);
130130
}
131131
}
132132

@@ -137,7 +137,7 @@ public static int maxCompressionLevel() {
137137
try {
138138
return (int) Bindings.MAX_C_LEVEL.invokeExact();
139139
} catch (Throwable t) {
140-
throw sneaky(t);
140+
throw NativeCall.rethrow(t);
141141
}
142142
}
143143

@@ -148,7 +148,7 @@ public static int minCompressionLevel() {
148148
try {
149149
return (int) Bindings.MIN_C_LEVEL.invokeExact();
150150
} catch (Throwable t) {
151-
throw sneaky(t);
151+
throw NativeCall.rethrow(t);
152152
}
153153
}
154154

@@ -159,7 +159,7 @@ public static int defaultCompressionLevel() {
159159
try {
160160
return (int) Bindings.DEFAULT_C_LEVEL.invokeExact();
161161
} catch (Throwable t) {
162-
throw sneaky(t);
162+
throw NativeCall.rethrow(t);
163163
}
164164
}
165165

@@ -172,7 +172,7 @@ public static long estimateCompressContextSize(int level) {
172172
try {
173173
return (long) Bindings.ESTIMATE_CCTX_SIZE.invokeExact(level);
174174
} catch (Throwable t) {
175-
throw sneaky(t);
175+
throw NativeCall.rethrow(t);
176176
}
177177
}
178178

@@ -183,7 +183,7 @@ public static long estimateDecompressContextSize() {
183183
try {
184184
return (long) Bindings.ESTIMATE_DCTX_SIZE.invokeExact();
185185
} catch (Throwable t) {
186-
throw sneaky(t);
186+
throw NativeCall.rethrow(t);
187187
}
188188
}
189189

@@ -197,7 +197,7 @@ public static long estimateCompressDictSize(long dictSize, int level) {
197197
try {
198198
return (long) Bindings.ESTIMATE_CDICT_SIZE.invokeExact(dictSize, level);
199199
} catch (Throwable t) {
200-
throw sneaky(t);
200+
throw NativeCall.rethrow(t);
201201
}
202202
}
203203

@@ -210,7 +210,7 @@ public static long estimateDecompressDictSize(long dictSize) {
210210
try {
211211
return (long) Bindings.ESTIMATE_DDICT_SIZE.invokeExact(dictSize, 0); // ZSTD_dlm_byCopy
212212
} catch (Throwable t) {
213-
throw sneaky(t);
213+
throw NativeCall.rethrow(t);
214214
}
215215
}
216216

@@ -223,69 +223,12 @@ public static String version() {
223223
MemorySegment p = (MemorySegment) Bindings.VERSION_STRING.invokeExact();
224224
return p.reinterpret(Long.MAX_VALUE).getString(0, StandardCharsets.US_ASCII);
225225
} catch (Throwable t) {
226-
throw sneaky(t);
226+
throw NativeCall.rethrow(t);
227227
}
228228
}
229229

230230
// --- package-private helpers shared with the context classes ---
231-
232-
/// A native call returning a zstd `size_t` status that may encode an error.
233-
@FunctionalInterface
234-
interface SizeCall {
235-
long run() throws Throwable;
236-
}
237-
238-
/// Invokes a size-returning zstd call and converts a zstd error code into a
239-
/// {@link ZstdException}.
240-
static long call(SizeCall c) {
241-
long code;
242-
try {
243-
code = c.run();
244-
} catch (Throwable t) {
245-
throw sneaky(t);
246-
}
247-
if (isError(code)) {
248-
throw new ZstdException(errorName(code), ZstdErrorCode.of(errorCode(code)));
249-
}
250-
return code;
251-
}
252-
253-
static boolean isError(long code) {
254-
try {
255-
return ((int) Bindings.IS_ERROR.invokeExact(code)) != 0;
256-
} catch (Throwable t) {
257-
throw sneaky(t);
258-
}
259-
}
260-
261-
private static int errorCode(long code) {
262-
try {
263-
return (int) Bindings.GET_ERROR_CODE.invokeExact(code);
264-
} catch (Throwable t) {
265-
throw sneaky(t);
266-
}
267-
}
268-
269-
@SuppressWarnings("restricted") // reinterpret needed to read a C string of unknown length
270-
private static String errorName(long code) {
271-
try {
272-
MemorySegment p = (MemorySegment) Bindings.GET_ERROR_NAME.invokeExact(code);
273-
return p.reinterpret(Long.MAX_VALUE).getString(0, StandardCharsets.US_ASCII);
274-
} catch (Throwable t) {
275-
throw sneaky(t);
276-
}
277-
}
278-
279-
/// Guards a zero-copy entry point: the segment handed to zstd must be backed
280-
/// by native (off-heap) memory, since its address is dereferenced in C. Fails
281-
/// fast with a clear message instead of the FFM linker's cryptic error.
282-
static MemorySegment requireNative(MemorySegment seg, String name) {
283-
if (!seg.isNative()) {
284-
throw new IllegalArgumentException(
285-
name + " must be a native (off-heap) MemorySegment; got a heap segment");
286-
}
287-
return seg;
288-
}
231+
// Native-call status checking and segment guards live in NativeCall.
289232

290233
static MemorySegment copyIn(Arena arena, byte[] src) {
291234
MemorySegment seg = arena.allocate(Math.max(src.length, 1));
@@ -299,11 +242,6 @@ static byte[] copyOut(MemorySegment seg, long len) {
299242
return out;
300243
}
301244

302-
@SuppressWarnings("unchecked")
303-
private static <E extends Throwable> RuntimeException sneaky(Throwable t) throws E {
304-
throw (E) t;
305-
}
306-
307245
private Zstd() {
308246
// no instances
309247
}
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.dfa1.zstd;
22

33
import java.lang.foreign.Arena;
4+
import java.lang.foreign.MemoryLayout.PathElement;
45
import java.lang.foreign.MemorySegment;
56
import java.lang.foreign.SegmentAllocator;
67
import java.lang.invoke.MethodHandle;
@@ -15,23 +16,30 @@
1516
/// @param upperBound the largest accepted value, inclusive
1617
public record ZstdBounds(int lowerBound, int upperBound) {
1718

19+
// Offsets into the returned ZSTD_bounds struct, derived from the named layout
20+
// rather than hand-counted, so they track the struct definition.
21+
private static final long ERROR_OFFSET = Bindings.BOUNDS_LAYOUT.byteOffset(PathElement.groupElement("error"));
22+
private static final long LOWER_OFFSET = Bindings.BOUNDS_LAYOUT.byteOffset(PathElement.groupElement("lowerBound"));
23+
private static final long UPPER_OFFSET = Bindings.BOUNDS_LAYOUT.byteOffset(PathElement.groupElement("upperBound"));
24+
1825
/// Calls a `*_getBounds` function (which returns a `ZSTD_bounds` struct by
1926
/// value: `{ size_t error; int lowerBound; int upperBound; }`).
2027
static ZstdBounds query(MethodHandle getBounds, int parameter) {
2128
try (Arena arena = Arena.ofConfined()) {
29+
// getBounds returns a ZSTD_bounds struct by value. For a struct return,
30+
// the FFM linker prepends a SegmentAllocator parameter to the handle:
31+
// it allocates BOUNDS_LAYOUT.byteSize() bytes from that allocator, the
32+
// native call writes the struct there, and the handle returns a segment
33+
// viewing it. Passing the arena makes the struct arena-owned (freed on
34+
// close); the cast satisfies invokeExact's exact-type requirement.
2235
MemorySegment bounds = (MemorySegment) getBounds.invokeExact((SegmentAllocator) arena, parameter);
23-
long error = bounds.get(JAVA_LONG, 0);
24-
if (Zstd.isError(error)) {
36+
long error = bounds.get(JAVA_LONG, ERROR_OFFSET);
37+
if (NativeCall.isError(error)) {
2538
throw new ZstdException("parameter has no queryable bounds");
2639
}
27-
return new ZstdBounds(bounds.get(JAVA_INT, 8), bounds.get(JAVA_INT, 12));
40+
return new ZstdBounds(bounds.get(JAVA_INT, LOWER_OFFSET), bounds.get(JAVA_INT, UPPER_OFFSET));
2841
} catch (Throwable t) {
29-
throw rethrow(t);
42+
throw NativeCall.rethrow(t);
3043
}
3144
}
32-
33-
@SuppressWarnings("unchecked")
34-
private static <E extends Throwable> RuntimeException rethrow(Throwable t) throws E {
35-
throw (E) t;
36-
}
3745
}

0 commit comments

Comments
 (0)