private static void demonstrateStrlen(Linker linker, SymbolLookup stdlib) throws Throwable {
    // Find strlen function: size_t strlen(const char *s)
    MemorySegment strlenAddress = stdlib.find("strlen").orElseThrow();

    // Create function descriptor: takes pointer, returns size_t
    FunctionDescriptor strlenDesc = FunctionDescriptor.of(
            ValueLayout.JAVA_LONG,    // return: size_t (long on most platforms)
            ValueLayout.ADDRESS       // param: const char* (pointer)
    );

    // Create method handle for the function
    MethodHandle strlen = linker.downcallHandle(strlenAddress, strlenDesc);

    // Create a C string in native memory
    try (Arena arena = Arena.ofConfined()) {
        // Test string
        var testString = "Hello FFM API!";

        // Allocate and copy UTF-8 string to native memory
        MemorySegment cString = arena.allocateFrom(testString);

        // Call strlen
        long length = (long) strlen.invoke(cString);

        IO.println("\tstrlen for '" + testString + "' = " + length);
    }
}

private static void demonstrateGetpid(Linker linker, SymbolLookup stdlib) throws Throwable {
    // Find getpid function: pid_t getpid(void)
    var getpidOpt = stdlib.find("getpid");
    if (getpidOpt.isPresent()) {
        MemorySegment getpidAddress = getpidOpt.get();

        // Function takes no parameters, returns int
        FunctionDescriptor getpidDesc = FunctionDescriptor.of(ValueLayout.JAVA_INT);
        MethodHandle getpid = linker.downcallHandle(getpidAddress, getpidDesc);

        // Call getpid
        int pid = (int) getpid.invoke();
        IO.println("\tCurrent process ID: " + pid);
    } else {
        IO.println("\tgetpid() not available on this platform");
    }
}

private static void demonstrateMallocFree(Linker linker, SymbolLookup stdlib) throws Throwable {
    // Find malloc: void* malloc(size_t size)
    MemorySegment mallocAddress = stdlib.find("malloc").orElseThrow();
    IO.println("\tFound malloc at " + Long.toHexString(mallocAddress.address()).toUpperCase());

    FunctionDescriptor mallocDesc = FunctionDescriptor.of(
            ValueLayout.ADDRESS,      // return: void*
            ValueLayout.JAVA_LONG     // param: size_t
    );
    MethodHandle malloc = linker.downcallHandle(mallocAddress, mallocDesc);

    // Find free: void free(void* ptr)
    MemorySegment freeAddress = stdlib.find("free").orElseThrow();
    FunctionDescriptor freeDesc = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS);
    MethodHandle free = linker.downcallHandle(freeAddress, freeDesc);

    // Allocate 1024 bytes
    MemorySegment ptr = (MemorySegment) malloc.invoke(1024L);

    if (!ptr.equals(MemorySegment.NULL)) {
        IO.println("\tmalloc(1024) = 0x" + Long.toHexString(ptr.address()).toUpperCase());

        // Create a segment view of the allocated memory
        MemorySegment allocatedMemory = ptr.reinterpret(1024);

        // Write some data
        int valueWrite = 0x12345678;
        allocatedMemory.setAtIndex(ValueLayout.JAVA_INT, 0, valueWrite);
        IO.println("\tWrote data into memory: 0x" + Integer.toHexString(valueWrite).toUpperCase());

        // Read it back
        int valueRead = allocatedMemory.getAtIndex(ValueLayout.JAVA_INT, 0);
        IO.println("\tRead back: 0x" + Integer.toHexString(valueRead).toUpperCase());

        // Free the memory
        free.invoke(ptr);
        IO.println("\tMemory freed with free()");
    }
}

/**
 * Execute with
 * java  --enable-native-access=ALL-UNNAMED FFMNativeCalls.java
 *
 * @throws Throwable
 */
void main() throws Throwable {
    IO.println();
    IO.println("=== FFM API Demo: Native Function Calls ===");
    IO.println();

    // Get a linker to link with native functions
    Linker linker = Linker.nativeLinker();

    // Look up native functions from the C standard library
    SymbolLookup stdlib = linker.defaultLookup();

    IO.println("Example 1: Call strlen() to get string length");
    demonstrateStrlen(linker, stdlib);
    IO.println();

    IO.println("Example 2: Call getpid() to get process ID");
    demonstrateGetpid(linker, stdlib);
    IO.println();

    IO.println("Example 3: Allocate memory with malloc() and free it");
    demonstrateMallocFree(linker, stdlib);
    IO.println();
}