Jextract を使って、Java から libcurl を使ってみる

（この記事は [地平線に行く](https://yujisoftware.hatenablog.com/entry/jextract_libcurl) とのマルチポストです）

Jextract に libcurl を呼び出すサンプルがありました。これを試しに動かしてみました。
（まだ Preview の [Foreign Function & Memory API](https://openjdk.java.net/jeps/424) を使います）

ちなみに、ほかにも以下のライブラリを使うサンプルが置いてあります。
|ディレクトリ|実行するライブラリ|
|---|---|
|cblas|OpenBLAS（行列演算ライブラリ）|
|lapack|LAPACK (数値解析ソフトウェアライブラリ）|
|libclang|Clang の C インタフェースライブラリ|
|libcurl|cURL（データ転送ライブラリ）|
|libffmpeg|FFmpeg（動画・音声の記録・変換・再生ライブラリ）|
|libgit2|Git（バージョン管理）|
|libjimage|ImageJ（画像処理ライブラリ）|
|libproc|/proc インターフェイス|
|lp_solve|lp_solve（混合整数線形計画法のソルバー）|
|opengl|OpenGL（2次元/3次元コンピュータグラフィックスライブラリ）|
|readline|Readline（コマンドライン対話ライブラリ）|
|sqlite|SQLite（データベース）|
|tcl|Tcl（ユーティリティモジュール）|
|tensorflow|TensorFlow（機械学習ライブラリ）|
|dlopen|動的リンクを行うローダー|
|time|C言語標準ライブラリの time|

また、Python3 スクリプトを実行するサンプル、Go との相互呼び出しを行うサンプルもおいてあります。

# 前置き

## [Jextract](https://github.com/openjdk/jextract) とは

> `jextract` is a tool which mechanically generates Java bindings from a native library headers. This tools leverages the [clang C API](https://clang.llvm.org/doxygen/group__CINDEX.html) in order to parse the headers associated with a given native library, and the generated Java bindings build upon the [Foreign Function & Memory API](https://openjdk.java.net/jeps/424). 
> 
> `jextract` は、ネイティブ ライブラリ ヘッダーから Java バインディングを機械的に生成するツールです。このツールは、[clang C API](https://clang.llvm.org/doxygen/group__CINDEX.html) を利用して、特定のネイティブ ライブラリに関連付けられたヘッダーを解析し、生成された Java バインディングは [Foreign Function & Memory API](https://openjdk.java.net/jeps/424) に基づいて構築されます。
> 
> [README.md](https://github.com/openjdk/jextract/blob/master/README.md)

## [Foreign Function & Memory API](https://openjdk.java.net/jeps/424) とは

> Introduce an API by which Java programs can interoperate with code and data outside of the Java runtime. By efficiently invoking foreign functions (i.e., code outside the JVM), and by safely accessing foreign memory (i.e., memory not managed by the JVM), the API enables Java programs to call native libraries and process native data without the brittleness and danger of JNI. This is a preview API.
>
> Java プログラムが Java ランタイム外のコードやデータと相互運用できる API を導入します。外部関数 (つまり、JVM の外部のコード) を効率的に呼び出し、外部メモリ (つまり、JVM によって管理されないメモリ) に安全にアクセスすることにより、API は Java プログラムがネイティブ ライブラリを呼び出し、ネイティブ データをもろくて危険なJNIを使わずに処理できるようにします。 これはプレビュー API です。
> 
> [Introduction](https://openjdk.org/jeps/424#Summary)

また、先日の JJUG ナイトセミナーの資料も併せてごらんください。
[JEP 424 Foreign Function & Memory API を試しに使ってみました！ - Speaker Deck](https://speakerdeck.com/yujisoftware/jep-424-foreign-function-and-memory-api-woshi-sinishi-tutemimasita)


## 今回動かすコード

[jextract の samples/libcurl/CurlMain.java](https://github.com/openjdk/jextract/blob/master/samples/libcurl/CurlMain.java)
引数の URL にアクセスして、取得した内容を標準出力に流すプログラムです。

```java
import java.lang.foreign.MemorySession;
import java.lang.foreign.SegmentAllocator;
import static java.lang.foreign.MemoryAddress.NULL;
import static org.jextract.curl_h.*;
import org.jextract.*;

public class CurlMain {
   public static void main(String[] args) {
       var urlStr = args[0];
       curl_global_init(CURL_GLOBAL_DEFAULT());
       var curl = curl_easy_init();
       if(!curl.equals(NULL)) {
           try (var session = MemorySession.openConfined()) {
               var url = session.allocateUtf8String(urlStr);
               curl_easy_setopt(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();
   }
}
```

# 手順

## 下準備（jextract のコンパイル）

Ubuntu 22.04 で作業しました。
WSL2 上でも同じはず。 Mac の場合は [README.md](https://github.com/openjdk/jextract/blob/master/README.md) を参照。

`apt` で必要なものをインストールします。

* Gradle 実行用の Java 17[^java]
* jextract のコンパイルに必要な LLVM, clang
* サンプルの実行に必要な libcurl (with OpenSSL)

[^java]: Gradle がまだ Java 19 未サポートなので別のバージョン (Java 17) をここでインストールしています。

```sh
sudo apt update
sudo apt install -y openjdk-17-jdk llvm-dev libclang-14-dev libcurl4-openssl-dev
```

JAVA_HOME を設定します。
```sh
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/
```

続いて、Java 19 を準備します。
どのディストリビューションでも構わないですが、今回は https://jdk.java.net/19/ のものを使用しました。

```sh
cd $HOME
curl -LO 'https://download.java.net/java/GA/jdk19/877d6127e982470ba2a7faa31cc93d04/36/GPL/openjdk-19_linux-x64_bin.tar.gz'
tar xf openjdk-19_linux-x64_bin.tar.gz
```

jextract をコンパイルします。

```sh
git clone git@github.com:openjdk/jextract.git
sh ./gradlew -Pjdk19_home=$HOME/jdk-19/ -Pllvm_home=/usr/lib/llvm-14 clean verify
```

ところが、エラーになりました。

```sh
> Error: the path /usr/lib/llvm-14/lib/clang/14/include does not exist
```

どうやら、パスが違うみたいです。パッチを当てます。

```diff
diff --git a/build.gradle b/build.gradle
index 1e8b620..c1b3cd7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,7 +20,7 @@ if (clang_versions.length == 0) {
     throw new IllegalArgumentException("Could not detect clang version." +
             " Make sure a ${llvm_home}/lib/clang/<VERSION> directory exists")
 }
-def clang_version = clang_versions[0]
+def clang_version = clang_versions[1]

 def jextract_version = "19"
 def jmods_dir = "$buildDir/jmods"
```

改めて実行します。

```sh
sh ./gradlew -Pjdk19_home=$HOME/jdk-19/ -Pllvm_home=/usr/lib/llvm-14 clean verify
```

今度はうまくいきました。
```sh
BUILD SUCCESSFUL in 38s
8 actionable tasks: 8 executed
```

`jextract` コマンドにパスを通しておきましょう。

```sh
export PATH=`pwd`/build/jextract/bin:$PATH 
```

## サンプルの実行

まずは、jextract で curl のバインディング用クラスを生成します。

```sh
cd samples/libcurl
jextract -t org.jextract -lcurl /usr/include/x86_64-linux-gnu/curl/curl.h
```

これで、 `org/jextract` 配下にクラスが作られます。
（ちなみに、上記のコマンドに `--source` を付与すれば、クラスのソースコードが作られます）


それでは、いよいよサンプルの実行です！
```
java --enable-native-access=ALL-UNNAMED \
    --enable-preview --source=19 \
    -Djava.library.path=/usr/lib/x86_64-linux-gnu CurlMain.java \
    'http://www.example.com/'
```

うまくいけば、がーっと html が流れてきます。
```html
Note: CurlMain.java uses preview features of Java SE 19.
Note: Recompile with -Xlint:preview for details.
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
（略）
```

# まとめ

Java からいろいろなライブラリを呼べるようになって、Java の可能性が一気に広がった感じがします。