A few FFM questions

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Tue Jul 11 00:09:40 UTC 2023


On 10/07/2023 16:02, Brian S O’Neill wrote:

    I want to avoid polluting the other thread, which no longer has a
    valid subject line anyhow…

    1) The size limit of a memory segment is defined by a signed value,
    thus limiting the address space to be 63-bit. Is it likely that all
    64-bit architectures today and in the future only use the lower half
    of the address range?

While it’s true that a memory segment can never be big enough as the 
full address space, in reality that limit is big enough - e.g. it is 
hard to imagine a developer wanting to express a region of memory whose 
size is bigger than 2^63. I believe the angle you are coming from here 
is that you want to use a memory segment to model the entire heap (at 
least judging from the other thread you started), and then wanting to 
just use a raw long pointer as an “offset” into the segment. Given the 
unsigned limitations, this will not work 100%: the FFM API specifies 
that the “long” argument you pass to MemorySegment::get (same for var 
handle) is a positive offset, relative to the start of the segment. That 
is, a negative offset is, by definition, /outside/ the memory segment. 
So, I do not see a way to add support for full unsigned long as memory 
segment sizes/offsets - in fact, I believe that allowing negative values 
in places where we expect an offset is almost always a smell, and a 
place for bugs to hide.

In terms of interfacing with native code, note that the FFM API can 
still wrap whatever address a native library throws at it, by wrapping 
the base address (a long) into a segment using MemorySegment::ofAddress. 
This method takes a raw address value, which can be negative. Of course 
the size of the segment will be limited to “only” Long.MAX_VALUE, which, 
seems reasonable enough.

While it’s theoretically possible to add a different kind of segment 
that behaves in a different way (e.g. allowing negative offsets), I 
believe the cost vs. benefit ratio for doing so would be very unfavourable.

    2) The MemorySegment copy operation is safe for use against
    overlapping ranges, and is thus a “memmove” operation. My application
    would benefit from also having a “memcpy” operation, for those cases
    where I know that the ranges don’t overlap. Can such an operation be
    added?
    This is not an issue, or at least not one that should be solved via
    new API surface: MemorySegment::copy relies on Unsafe::copyMemory
    which does memmove vs. memcpy depending on wether it can prove that
    ranges overlap. Rather than duplicating the API, if we find cases
    where the existing logic doesn’t work well, we should probably
    invest in rectifying and/or improving that logic.

    3) Dynamic memory allocation is performed against Arenas, but freeing
    the memory is only allowed when the Arena is closed. I find this to
    be cumbersome at times, and in one case I ended up creating a single
    Arena instance paired with each allocation. The other solution is to
    directly call malloc/free, which is what I’m using in some places.
    Both solutions are messy, so is it possible to add a simple
    malloc/free API?

The API embraces the idea that one lifetime might be associated with 
multiple allocations quite deeply (this is explained in more details 
here [1]). While this is an arbitrary choice (after all, there is no 
perfect way to deal with deallocation of native resources in a way that 
is both safe and efficient), malloc/free is not the right primitive to 
design a safe API to manage off-heap resources for at least two reasons:

  * it is too fine-grained, there’s no way to group together
    logically-related resources (think of the relationship between
    dlopen, dlsym and dlclose)
  * it is not safe-by-default: anyone can free a pointer, even one they
    did not create

In contrast, the lifetime-centric approach adopted by the FFM API allows 
developers to group logically related segments in the same lifetime, 
which is, often, a very useful move, and allows the API to scale for 
managing lifetime of things that are not just malloc’ed segments (such 
specifying lifetime of an upcall stub, or that of a native library 
loaded with dlopen). This allows the API to detect pesky conditions such 
as memory leaks and/or use-after free. There are of course cases where 
this way of doing things is not a perfect fit, and a lower-level access 
to malloc/free is preferrable. In these cases, developers can, as you 
observed, just call malloc/free downcall method handles, and deal with 
memory allocation/deallocation completely manually. Or they can create a 
one-off arena to deal with that allocation.

To do the latter, you probably would need some kind of abstraction on top:

|record HeapAllocation(MemorySegment segment, Arena arena) implements 
AutoCloseable { public close() { arena().close(); } static 
HeapAllocation mallocConfined(long bytes) { Arena arena = 
Arena.ofConfined(); return new HeapAllocation(arena.allocate(bytes), 
arena); } } |

To do the former you need to write some wrappers for malloc/free:

|static final MethodHandle MALLOC = Linker.nativeLinker().... static 
final MethodHandle FREE = Linker.nativeLinker().... static MemorySegment 
malloc(long bytes) { return MALLOC.invokeExact(bytes); } static 
MemorySegment free(MemorySegment segment) { return 
FREE.invokeExact(segment); } |

Of course the former approach provides more temporal safety (but might 
also result in more overhead for enforcing said safety, which, I’m not 
sure you’d be too happy with). In the latter, what you see is what you 
get, you are playing the “power user” card, ensuring correctness (read: 
avoid use-after-free) is now up to you.

I don’t think either approach looks too “messy”. Of course 
one-lifetime-per-segment is not the ideal sweet spot the FFM API is 
designed for, and one has to write some extra code, but the FFM API 
still allows you to do what you need to do (or to completely bypass 
temporal safety alltogether, if you decide to do so). After having spent 
considerable time looking at possible approaches to deal with memory 
safety, we did not find a “simpler malloc/free API” that was good enough 
as a building block for managing temporal resources in the FFM API.

    4) GuardUnsafeAccess. I understand that this was added to ensure that
    accessing a memory mapped file which was been truncated doesn’t crash
    the JVM. What is the overhead of this check? Given that my module
    requires restricted access anyhow, it can already crash the JVM.
    Would it be possible to support unguarded memory access operations
    too?

This seems another case (like memcpy vs memmove) where it is assumed 
there’s some overhead associated to operation XYZ (which the JVM does to 
ensure some kind of safety in the common case), hence the request for a 
backdoor. I’d like to turn these kind of arguments around and instead 
ask for some benchmark showing that the costs associated with these 
operations are too high (and, if so, we might be able to find ways to 
improve the status quo w/o necessarily changing the API).

Cheers
Maurizio

[1] - https://cr.openjdk.org/~mcimadamore/panama/why_lifetimes.html

​
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/panama-dev/attachments/20230711/d1b3ed8b/attachment-0001.htm>


More information about the panama-dev mailing list