Bootstrap

JAVA22 FFM实战之HelloWorld

前言

JDK22即将发布,Java Foreign Function & Memory API将会退出预览,是时候开始学习一波了。

FFM API介绍

FFM API由两大部分组成,一个是Foreign Function Interface,另一个是Memory API。前者是外部函数接口,简称FFI,用它来实现Java代码和外部代码之间相互操作;后者是内存接口,用于安全地管理堆外内存。

上面说这么多,可以简单的认为是jni的替代品。

本文不关注底层如何实现,一两个常用的c库为例子,重点在了解api的使用以及使用jextract生成java代码

入门

使用c语言的strlen和printf格式化打印helloworld

/**
 * @author authorZhao
 * @since 2024-03-13
 */
public class FFMHelloWorld {
    public static void main(String[] args) {
        Linker linker = Linker.nativeLinker();
        SymbolLookup defaultLookup = linker.defaultLookup();
        MethodHandle strlenHandle = linker.downcallHandle(
                defaultLookup.find("strlen").orElseThrow(),
                FunctionDescriptor.of(JAVA_LONG, ADDRESS));
        MethodHandle printfHandler = linker.downcallHandle(
                defaultLookup.find("printf").orElseThrow(),
                FunctionDescriptor.of(JAVA_LONG, ADDRESS,JAVA_INT));
        try (Arena offHeap = Arena.ofConfined()) {
            MemorySegment pointers = offHeap.allocateUtf8String("Hello world%d!");
            System.out.println(strlenHandle.invoke(pointers));  //11
            printfHandler.invoke(pointers,8);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

实战级别的HelloWorld

工具准备

下载curl

下载lua

  • 注意需要下载源码最好,有条件的自行编译,编译为动态库

  • 效果如图,本文只需要dll即可没动态库名字我自己改的

  • 头文件目录

在这里插入图片描述
在这里插入图片描述
这里的cjson已经被移动到同级目录的dll目录下面

下载jextract 本文选择jdk22的版本

打开jextract的github链接

一、CURL

打开curl的例子 https://github.com/openjdk/jextract/tree/master/samples/libcurl

windows脚本如下

param(
  [Parameter(Mandatory=$true, HelpMessage="The path to the lib curl installation")]
  [string]$curlpath
)

jextract `
  -I "$curlpath\include" `
  -I "$curlpath\include\curl" `
  --dump-includes 'includes_all.conf' `
  "$curlpath\include\curl\curl.h"
  
Select-String -Path 'includes_all.conf' -Pattern '(curl|sockaddr )' | %{ $_.Line } | Out-File -FilePath 'includes_filtered.conf' -Encoding ascii

jextract `
  --output src `
  -t org.jextract `
  -I "$curlpath\include" `
  -I "$curlpath\include\curl" `
  -llibcurl `
  '@includes_filtered.conf' `
  "$curlpath\include\curl\curl.h"

javac -d classes (ls -r src/*.java)

本人以自己的curl为例

生成脚本

本人的curlpath = E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw

我按照github执行了两次命令

1. 这个执行完毕得到一个includes_filtered.conf文件
jextract --output src -t org.jextract -I "E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw/include" -I "E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw/include/curl" -llibcurl '@includes_filtered.conf' "E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw/include/curl/curl.h"

2.这个执行完毕直接得到生成的java代
jextract --output src -t org.jextract -I "E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw/include" -I "E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw/include/curl" "E:/tool/curl-8.6.0_6-win64-mingw/curl-8.6.0_6-win64-mingw/include/curl/curl.h"

拷贝生成java代码(好家伙52M)到idea

生成的工具有一个方法和我本地jdk有点不一致(可能是版本原因,本地jdk21,工具依赖的jdk是22), ctrl+shift+r 全局替换一下getUtf8String

arena.allocateFrom(urlStr); 替换为  arena.getUtf8String(urlStr);

启动

import java.lang.foreign.Arena;

import static java.lang.foreign.MemorySegment.NULL;
import static org.jextract.curl_h.*;

public class CurlMain {
    static {
        System.load("E:\\tool\\curl-8.6.0_6-win64-mingw\\curl-8.6.0_6-win64-mingw\\bin\\libcurl-x64.dll");
    }
   public static void main(String[] args) {
       var urlStr = "https://www.baidu.com";
       curl_global_init(CURL_GLOBAL_DEFAULT());
       var curl = curl_easy_init();
       //curl_easy_setopt
       if (!curl.equals(NULL)) {
           try (var arena = Arena.ofConfined()) {
               var url = arena.allocateUtf8String(urlStr);
               //忽略ssl忽略证书检查
               curl_easy_setopt.makeInvoker(C_LONG_LONG).apply(curl, CURLOPT_SSL_VERIFYPEER(), 0);
               curl_easy_setopt.makeInvoker(C_LONG_LONG).apply(curl, CURLOPT_URL(), url.address());
               int res = curl_easy_perform(curl);
               if (res != CURLE_OK()) {
                   String error = curl_easy_strerror(res).getUtf8String(0);
                   System.out.println("Curl error: " + error);
                   curl_easy_cleanup(curl);
               }
           }
       }
       curl_global_cleanup();
   }
}

curl的java执行结果

在这里插入图片描述

二、LUA

lua源代码

print("package.cpath=" .. package.cpath)
-- 注意cjson不在lua.exe同级目录,再下一级别
package.cpath = package.cpath .. ';E:/tool/lua5.4-zhao/dll/?.dll'
local cjson = require("cjson")

function test()
    local map = {}
    map["a"] = "张三"
    map["b"] = "李四"
    for k,v in pairs(map) do
        print(k .. '=' .. v)
    end
    print(cjson.encode(map))
    return 100
end
test()

lua源代码的直接执行结果

在这里插入图片描述

生成命令

生成代码并,拷贝代码到idea

jextract --output src -t com.lua -I "E:/tool/lua-5.4.4/src" "E:/tool/lua-5.4.4/src/lua.h" -l :E:/tool/lua5.4-zhao/liblua5.4.0.dll

jextract --output src -t com.lua -I "E:/tool/lua-5.4.4/src" "E:/tool/lua-5.4.4/src/lualib.h" -l :E:/tool/lua5.4-zhao/liblua5.4.0.dll

jextract --output src -t com.lua -I "E:/tool/lua-5.4.4/src" "E:/tool/lua-5.4.4/src/lauxlib.h" -l :E:/tool/lua5.4-zhao/liblua5.4.0.dll

在这里插入图片描述

idea导入生成的java代码

/**
 * @author authorZhao
 */
public class LuaMain {
    static {
        //这里不适用load,生成代码使用-l指定动态库的位置,生成的代码直接包含了,但是这种方式用的是绝对路径,使用SymbolLookup.libraryLookup
        //System.load("E:/tool/lua5.4-zhao/liblua5.4.0.dll");
    }

    public static void main(String[] args) {
        var lua_State = luaL_newstate();
        if (lua_State == NULL) {
            return;
        }
        //打开内置库
        luaL_openlibs(lua_State);
        try (var arena = Arena.ofConfined()) {
            var code = arena.allocateUtf8String("print(\"渣渣辉\") return \"嘿嘿\"");
            //加载而不执行
            int i = luaL_loadstring(lua_State, code);
            if (i != 0) {
                return;
            }
            // pcall
            lua_h.lua_pcallk(lua_State, 0, 1, 0, 0L, NULL);
            var data2 = lua_h.lua_tolstring(lua_State, -1,NULL);
            System.out.println("data2 = " + data2.getUtf8String(0L));

            //这里的utf-8可能对中文路径有问题,英文影响应该不大
            int loadResult = luaL_loadfilex(lua_State, arena.allocateUtf8String("E:/tool/lua5.4-zhao/test2.lua"), NULL);
            System.out.println("loadResult = " + loadResult);

            // pcall
            int iRet = lua_h.lua_pcallk(lua_State, 0, 1, 0, 0L, NULL);

            if (iRet==0){
                var data = lua_h.lua_tointegerx(lua_State, -1,NULL);
                System.out.println("data = " + data);
            }

        }
        //关闭lua虚拟机
        lua_h.lua_close(lua_State);
    }

}

执行结果

在这里插入图片描述

本文运行环境

本文系统win10 运行jdk21,jextract工具是jdk22的版本,lua5.4

使用总结

  • curl本身就3M生成的代码文件有55M,编译的class有2万多个(可能是姿势不对,跟多类都没用到)
  • 体验还行,但是如果不了解这个c库,可能很多api都不会用,例如图上的ssl证书,不处理https都报错,lua的capi
  • 该工具目前无法识别c++的语法,以qt为例目前使用失败(有可能是使用姿势不对),理论上是要支持的
  • 以lua为例,一些宏无法识别
  • lua里面使用别的dll或者so,搜索路径是jdk下面开头的,这个需要注意
  • System.load是以前的jni加载动态库的方式,在这里照样可以这么用,网上查了一下SymbolLookup.libraryLookup会忽略jni_onload等函数
  • 生成代码的时候如果使用绝对路径指定动态库的位置,可能会造成跨平台问题,这里需要注意

参考

jextract的github地址 https://github.com/openjdk/jextract

lua5.4文档 https://www.lua.org/manual/5.4/

lua5.3中文文档(这里面对capi翻译的很详细) https://www.runoob.com/manual/lua53doc/manual.html#lua_tolstring

openjdk的jextract下载地址 https://jdk.java.net/jextract/

本文为原创,转载请申明

;