-
Enhancement
-
Resolution: Fixed
-
P3
-
None
-
repo-panama
Consider the following simple native library:
//libTest.c
struct BigStruct {
long long x;
long long y;
long long z;
long long u;
long long w;
};
void func(struct BigStruct s) { }
And the following FFM code:
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.StructLayout;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class BadPuts {
static {
System.loadLibrary("Test");
}
static final Linker linker = Linker.nativeLinker();
static final StructLayout BIG_STRUCT = MemoryLayout.structLayout(
ValueLayout.JAVA_LONG.withName("x"),
ValueLayout.JAVA_LONG.withName("y"),
ValueLayout.JAVA_LONG.withName("z"),
ValueLayout.JAVA_LONG.withName("u"),
ValueLayout.JAVA_LONG.withName("w")
);
static final MethodHandle bad_puts = linker.downcallHandle(
SymbolLookup.loaderLookup().find("func").get(),
FunctionDescriptor.ofVoid(BIG_STRUCT)
);
public static void main(String[] args) throws Throwable {
bad_puts.invokeExact(MemorySegment.ofArray(new byte[100])); // 1
bad_puts.invokeExact(MemorySegment.ofAddress(1)); // 2
}
}
In (1), we try to pass an heap segment to the downcall. This works on both Linux/x64 and Windows/x64.
In (2), we try to pass a zero-length memory segment to the downcall. This fails with an exception on both Linux/x64 and Windows/x64.
Discussion: the calling convention rules on by-value structs are very platform specific. The fact that (1) works on both platforms, but (2) does not seems to rely on the fact that a by-value struct is decomposed into its constituent fields _before_ the native call takes place - and then such fields copied on the stack.
This means that, for (1) we will just copy the contents of the heap segment on the stack, and then proceed. For (2) when trying to copy we will hit an issue, as the source segment is not big enough (hence the exception).
This behavior is not specified anywhere, and I wonder if this will be true for all ABIs. In principle it should: when passing a struct by value we do not expect the function to be able to alter the struct contents, so a copy must be taken. But, on paper, some arcane APIs might allow for copy-on-write policies, who knows? Should we just trust the underlying ABI to "do the right thing" here?
Given all this, I think the Linker javadoc should do one of the two things:
* specify that segments corresponding to by-value structs will be rejected if they are heap segments, or if they are not big enough.
* specify that the behavior when passing segments as by-value struct is platform-specific.
//libTest.c
struct BigStruct {
long long x;
long long y;
long long z;
long long u;
long long w;
};
void func(struct BigStruct s) { }
And the following FFM code:
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.StructLayout;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class BadPuts {
static {
System.loadLibrary("Test");
}
static final Linker linker = Linker.nativeLinker();
static final StructLayout BIG_STRUCT = MemoryLayout.structLayout(
ValueLayout.JAVA_LONG.withName("x"),
ValueLayout.JAVA_LONG.withName("y"),
ValueLayout.JAVA_LONG.withName("z"),
ValueLayout.JAVA_LONG.withName("u"),
ValueLayout.JAVA_LONG.withName("w")
);
static final MethodHandle bad_puts = linker.downcallHandle(
SymbolLookup.loaderLookup().find("func").get(),
FunctionDescriptor.ofVoid(BIG_STRUCT)
);
public static void main(String[] args) throws Throwable {
bad_puts.invokeExact(MemorySegment.ofArray(new byte[100])); // 1
bad_puts.invokeExact(MemorySegment.ofAddress(1)); // 2
}
}
In (1), we try to pass an heap segment to the downcall. This works on both Linux/x64 and Windows/x64.
In (2), we try to pass a zero-length memory segment to the downcall. This fails with an exception on both Linux/x64 and Windows/x64.
Discussion: the calling convention rules on by-value structs are very platform specific. The fact that (1) works on both platforms, but (2) does not seems to rely on the fact that a by-value struct is decomposed into its constituent fields _before_ the native call takes place - and then such fields copied on the stack.
This means that, for (1) we will just copy the contents of the heap segment on the stack, and then proceed. For (2) when trying to copy we will hit an issue, as the source segment is not big enough (hence the exception).
This behavior is not specified anywhere, and I wonder if this will be true for all ABIs. In principle it should: when passing a struct by value we do not expect the function to be able to alter the struct contents, so a copy must be taken. But, on paper, some arcane APIs might allow for copy-on-write policies, who knows? Should we just trust the underlying ABI to "do the right thing" here?
Given all this, I think the Linker javadoc should do one of the two things:
* specify that segments corresponding to by-value structs will be rejected if they are heap segments, or if they are not big enough.
* specify that the behavior when passing segments as by-value struct is platform-specific.