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
51 changes: 50 additions & 1 deletion codegen/src/main/java/FunctionsGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,20 @@ private boolean isTemporalCType(String cType) {
return TIMESTAMP_C_TYPES.contains(cleaned);
}

/**
* Returns true if the C return type is an OWNED {@code char *} that the
* caller must free: a non-const {@code char *}. {@code const char *}
* returns are borrowed/static (e.g. temporal_interp, temporal_subtype,
* geo_typename) and must NOT be freed. For an owned char* the interface
* binds the return as Pointer and the wrapper frees it after copying the
* string, instead of letting JNR-FFI copy the C string to a Java String
* and leak the original allocation.
*/
private boolean isOwnedCharReturn(String retCType) {
if (retCType.contains("const")) return false;
return retCType.replaceAll("\\s+", "").equals("char*");
}

// bool+result strategy: driven by the pointed-to C type
//
// The original version always generated:
Expand Down Expand Up @@ -443,6 +457,29 @@ private String generateFile(List<FunctionDef> functions) {
""");

sb.append("public class GeneratedFunctions {\n");
// Native deallocator for char* returned by owning MEOS functions.
// MEOS standalone allocates with the system malloc (palloc/pfree map to
// malloc/free outside PostgreSQL); freeMemory() calls the system free
// underneath. Uses sun.misc.Unsafe rather than a JNR-FFI libc binding to
// avoid classloader-boundary issues, mirroring MobilitySpark MeosMemory.
sb.append("""
\tprivate static final sun.misc.Unsafe _UNSAFE;
\tstatic {
\t\ttry {
\t\t\tjava.lang.reflect.Field _f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
\t\t\t_f.setAccessible(true);
\t\t\t_UNSAFE = (sun.misc.Unsafe) _f.get(null);
\t\t} catch (ReflectiveOperationException _e) {
\t\t\tthrow new ExceptionInInitializerError(_e);
\t\t}
\t}

\t/** Free a char* returned by an owning (non-const) MEOS function. Null-safe. */
\tprivate static void _freeCStr(Pointer _p) {
\t\tif (_p != null) _UNSAFE.freeMemory(_p.address());
\t}

""");
sb.append(generateAllInterfaces(functions, partSize));
sb.append("\n\n");

Expand Down Expand Up @@ -476,8 +513,11 @@ private String generateAllInterfaces(List<FunctionDef> functions, int partSize)
sb.append("\tpublic interface MeosLibraryPart").append(letters[p]).append(" {\n\n");
for (int i = start; i < end; i++) {
FunctionDef fn = functions.get(i);
// Owned char* returns bind as Pointer so the wrapper can free
// the native allocation after copying the string.
String ifaceRet = isOwnedCharReturn(fn.retCType) ? "Pointer" : fn.returnType;
sb.append("\t\t")
.append(fn.returnType).append(" ")
.append(ifaceRet).append(" ")
.append(fn.name).append("(")
.append(buildInterfaceParamList(fn.params))
.append(");\n\n");
Expand Down Expand Up @@ -690,6 +730,15 @@ private String generateStaticMethod(FunctionDef fn, int partIndex) {
} else if (fn.returnType.equals("void")) {
sb.append("\t\t").append(call).append("\n");
sb.append("\t\tMeosErrorHandler.checkError();\n");
} else if (isOwnedCharReturn(fn.retCType)) {
// Interface returns Pointer (owned char*). Copy the string, free the
// native allocation, and return the Java String — no leak.
sb.append("\t\tPointer _result = ").append(call).append("\n");
sb.append("\t\tMeosErrorHandler.checkError();\n");
sb.append("\t\tif (_result == null) return null;\n");
sb.append("\t\tString _str = _result.getString(0);\n");
sb.append("\t\t_freeCStr(_result);\n");
sb.append("\t\treturn _str;\n");
} else {
sb.append("\t\tvar _result = ").append(call).append("\n");
sb.append("\t\tMeosErrorHandler.checkError();\n");
Expand Down
25 changes: 25 additions & 0 deletions codegen/src/test/java/FunctionsGeneratorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class FunctionsGeneratorTest {
private Method mapCTypeToJava;
private Method mapCTypeToJavaWrapper;
private Method isTemporalCType;
private Method isOwnedCharReturn;
private Method resolveResultStrategy;
private Method sanitizeParamName;
private Method collectEnumNames;
Expand All @@ -35,6 +36,7 @@ void setUp() throws Exception {
mapCTypeToJava = reflect("mapCTypeToJava", String.class);
mapCTypeToJavaWrapper = reflect("mapCTypeToJavaWrapper", String.class);
isTemporalCType = reflect("isTemporalCType", String.class);
isOwnedCharReturn = reflect("isOwnedCharReturn", String.class);
sanitizeParamName = reflect("sanitizeParamName", String.class);
run = reflect("run", String.class, String.class);

Expand Down Expand Up @@ -79,6 +81,10 @@ private boolean isTemporal(String cType) throws Exception {
return (boolean) isTemporalCType.invoke(generator, cType);
}

private boolean isOwnedChar(String retCType) throws Exception {
return (boolean) isOwnedCharReturn.invoke(generator, retCType);
}

private Object resolveStrategy(String cType) throws Exception {
return resolveResultStrategy.invoke(generator, cType);
}
Expand Down Expand Up @@ -229,6 +235,25 @@ class IsTemporalCTypeTests {
@Test void double_false() throws Exception { assertFalse(isTemporal("double")); }
}

// =========================================================================
// isOwnedCharReturn
// =========================================================================

@Nested
@DisplayName("isOwnedCharReturn")
class IsOwnedCharReturnTests {

@Test void ownedCharPtr_true() throws Exception { assertTrue(isOwnedChar("char *")); }
@Test void ownedCharPtrNoSpace_true() throws Exception { assertTrue(isOwnedChar("char*")); }

@Test void constCharPtr_false() throws Exception { assertFalse(isOwnedChar("const char *")); }
@Test void doubleCharPtr_false() throws Exception { assertFalse(isOwnedChar("char **")); }
@Test void text_false() throws Exception { assertFalse(isOwnedChar("text *")); }
@Test void Pointer_false() throws Exception { assertFalse(isOwnedChar("Temporal *")); }
@Test void void_false() throws Exception { assertFalse(isOwnedChar("void")); }
@Test void int_false() throws Exception { assertFalse(isOwnedChar("int")); }
}

// =========================================================================
// resolveResultStrategy
// =========================================================================
Expand Down
Loading
Loading