概要
C言語のソースコードからJavaメソッドを実行し、結果を取得します。
取得する戻り値の型は、getArrayメソッドからのString[]をC言語のchar[][30]で、
cowSayメソッドからのString型をC言語のchar*として取得します。
実行環境
- Windows 11
- WSL(Ubuntu-24.04)
フォルダ構成
.
├── Moo.class
├── Moo.java
├── c_linux_build.bat
├── c_windows_build.bat
├── java_build.bat
├── javap-exec.bat
├── jni_exec
├── jni_exec.c
├── jni_exec.exe
├── lib
│ └── cowsay-1.1.0.jar
├── start_linux_exec.bat
└── start_windows_exec.bat
Cから呼び出す用のJavaコード
import com.github.ricksbrown.cowsay.Cowsay;
import java.util.Random;
public class Moo {
/*
* mainは使用しない
*/
public static void main(String[] args) {
String[] arr = getArray();
System.out.println(String.join(", ", arr));
var num = new Random().nextInt(arr.length);
System.out.println(cowSay(arr[num] + "said ,Hello from Java!", arr[num]));
}
/*
* 話す動物の一覧を取得
*/
public static String[] getArray() {
String[] args = new String[] { "-l" };
return Cowsay.say(args).trim().split(" ");
}
/*
* 動物に話させる
*/
public static String cowSay(String message, String animal) {
String[] args = new String[] { "-f", animal, message };
return Cowsay.say(args);
}
}
- 外部ライブラリも使えることを確認するために適当にCowsayライブラリを利用
Javaを呼び出すCコード
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <jni.h>
int animal_list(char*, char[][30]);
void cowsay(char*, char*, char*);
int main() {
char sep = ':';
#ifdef _WIN32
// Windows用
sep = ';';
#endif
char java_classpath[1024];
char *classpath;
if ((classpath = getenv("CLASSPATH")) == 0) {
puts("環境変数CLASSPATHがありません");
return -1;
}
// 外部ライブラリを利用するためには、JVM起動設定に指定する必要がある。
// ライブラリ情報は、環境変数CLASSPATHの値を利用する。
sprintf(java_classpath, "-Djava.class.path=.%c%s", sep, classpath);
printf("CLASSPATH: %s\n", java_classpath);
char arr[50][30];
int max_idx = animal_list(java_classpath, arr);
int i;
char output[2048];
srand(time(NULL));
// ランダムに10個表示
for (i = 0; i < 10; i++) {
memset(output, 0, sizeof(output));
cowsay(java_classpath, arr[rand() % max_idx], output);
printf("<<Result from Java>>\n%s\n", output);
}
return 0;
}
int animal_list(char* classpath, char result[][30]) {
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3]; // options[5];
options[0].optionString = classpath; /* クラスパス指定 */
options[1].optionString = "-Xss32m"; /* セグメンテーション違反防止 */
options[2].optionString = "-Xint"; /* JIT無効化 */
// options[3].optionString = "-Xcheck:jni"; /* オプションのチェック */
// options[4].optionString = "-verbose:jni"; /* JNI関係のメッセージを表示 */
vm_args.options = options;
vm_args.nOptions = 3; // 5
vm_args.version = JNI_VERSION_21;
vm_args.ignoreUnrecognized = JNI_FALSE;
int ret, nVMs;
ret = JNI_GetCreatedJavaVMs(&jvm, 1, &nVMs);
if (ret == JNI_OK && nVMs >= 1) {
// 生成済みのJMを再利用(JMは一つだけ、Create->Destory->Createは許されない)
puts("<VM再利用>");
ret = (*jvm)->AttachCurrentThread(jvm, (void **)&env, &vm_args);
} else {
puts("<VM新規作成>");
ret = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
}
if (ret != JNI_OK)
exit(-1);
jclass cls;
// Mooクラスを取得
if ((cls = (*env)->FindClass(env, "Moo")) == 0) {
(*env)->ExceptionDescribe(env);
exit(-2);
}
// getArrayメソッドを取得
jmethodID id = (*env)->GetStaticMethodID(
env, cls, "getArray",
"()[Ljava/lang/String;");
if (id == 0) {
(*env)->ExceptionDescribe(env);
exit(-3);
}
jarray jobj;
// メソッドを実行。引数無しの場合はNULL(=(void*)0)を指定する
if ((jobj = (jarray)(*env)->CallStaticObjectMethod(env, cls, id, 0)) == 0) {
(*env)->ExceptionDescribe(env);
exit(-4);
}
// 戻り値の配列の要素数を取得
int array_length = (*env)->GetArrayLength(env, jobj);
int i;
for (i = 0; i < array_length; i++) {
// java.lang.String[]から値を取り出し
jstring str = (jstring)(*env)->GetObjectArrayElement(env, jobj, i);
// java.lang.Stringをchar*に変更し取得
const char *native_string = (*env)->GetStringUTFChars(env, str, 0);
memset(result[i], 0, strlen(native_string) + 1);
strncpy(result[i], native_string, strlen(native_string));
// ネイティブメモリでメモリリークを起こすため解放
(*env)->ReleaseStringUTFChars(env, str, native_string);
}
return array_length;
}
void cowsay(char* classpath, char* animal, char* result) {
JavaVM *jvm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
options[0].optionString = classpath; /* クラスパス指定 */
options[1].optionString = "-Xss32m"; /* セグメンテーション違反防止 */
options[2].optionString = "-Xint"; /* JIT無効化 */
// options[3].optionString = "-Xcheck:jni"; /* オプションのチェック */
// options[4].optionString = "-verbose:jni"; /* JNI関係のメッセージを表示 */
vm_args.options = options;
vm_args.nOptions = 3;
vm_args.version = JNI_VERSION_21;
vm_args.ignoreUnrecognized = JNI_FALSE;
int ret, nVMs;
ret = JNI_GetCreatedJavaVMs(&jvm, 1, &nVMs);
if (ret == JNI_OK && nVMs >= 1) {
// 生成済みのJMを再利用(JMは一つだけ、Create->Destory->Createは許されない)
puts("<VM再利用>");
ret = (*jvm)->AttachCurrentThread(jvm, (void **)&env, &vm_args);
} else {
puts("<VM新規作成>");
ret = JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);
}
if (ret != JNI_OK)
exit(-1);
jclass cls;
// Mooクラスを取得
if ((cls = (*env)->FindClass(env, "Moo")) == 0) {
(*env)->ExceptionDescribe(env);
exit(-2);
}
// getArrayメソッドを取得
jmethodID id = (*env)->GetStaticMethodID(
env, cls, "cowSay",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
if (id == 0) {
(*env)->ExceptionDescribe(env);
exit(-3);
}
char message[100];
sprintf(message, "%s said, Hello from JNI!", animal);
jstring arg1 = (*env)->NewStringUTF(env, message);
jstring arg2 = (*env)->NewStringUTF(env, animal);
// メソッド実行。jstringはjvalueに入れることができないため、1つずつ引数に指定する。
jstring str;
if ((str = ((jstring)(*env)->CallStaticObjectMethod(env, cls, id, arg1, arg2))) == 0) {
(*env)->ExceptionDescribe(env);
exit(-4);
}
// java.lang.Stringをchar*に変更
const char *native_string = (*env)->GetStringUTFChars(env, str, 0);
strncpy(result, native_string, strlen(native_string));
// ネイティブメモリでメモリリークを起こすため解放
(*env)->ReleaseStringUTFChars(env, str, native_string);
}
ビルド手順
共通手順
必要なパッケージインストール
- JDK
Javaコンパイル用のバッチ
下記のビルドを成功させるためには、JDKのインストール、及び
batファイル用のフォルダ内にlibフォルダを作り、
その中にcowsay-1.1.0.jarを入れる必要があります。
.
└── lib
└── cowsay-1.1.0.jar
Windows環境側のjavacによりコンパイル
chcp 65001
set JAVA_HOME=C:\Program Files\Java\jdk-24
set PATH=%JAVA_HOME%\bin;%PATH%
@rem WSL側のOpenJDKが21のため、21を指定
javac --release 21 -proc:none -cp .;lib/cowsay-1.1.0.jar Moo.java
pause
java -cp .;lib/cowsay-1.1.0.jar Moo
pause
WSL環境側のjavacによりコンパイル
chcp 65001
set command=export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64;^
export PATH=$JAVA_HOME/bin:$PATH;^
javac --release 21 -proc:none -cp .:lib/cowsay-1.1.0.jar Moo.java;
wsl -e bash -l -c "%command%"
pause
wsl -e bash -l -c "java -cp .:lib/cowsay-1.1.0.jar Moo;"
pause
bash -lオプションを利用すると~/.bash_profileが読まれるようになります。
今回の例では、どの環境変数を使うかを明示するために全て記載していますが、
~/.bash_profile内でexportすれば、バッチ側に書く必要が無くなります。
Javaを単独で動かした場合の結果
beavis.zen, bud-frogs, bunny, cheese, cower, daemon, default, dragon, dragon-and-cow, elephant, elephant-in-snake, eyes, flaming-sheep, ghostbusters, hellokitty, kiss, kitty, koala, kosh, luke-koala, meow, milk, moofasa, moose, mutilated, ren, satanic, sheep, skeleton, small, squirrel, stegosaurus, stimpy, supermilker, surgery, telebears, three-eyes, turkey, turtle, tux, udder, vader, vader-koala, www
____________________________________
< ghostbusterssaid ,Hello from Java! >
------------------------------------
\
\
\ __---__
_- /--______
__--( / \ )XXXXXXXXXXX\v.
.-XXX( O O )XXXXXXXXXXXXXXX-
/XXX( U ) XXXXXXX\
/XXXXX( )--_ XXXXXXXXXXX\
/XXXXX/ ( O ) XXXXXX \XXXXX\
XXXXX/ / XXXXXX \__ \XXXXX
XXXXXX__/ XXXXXX \__---->
---___ XXX__/ XXXXXX \__ /
\- --__/ ___/\ XXXXXX / ___--/=
\-\ ___/ XXXXXX '--- XXXXXX
\-\/XXX\ XXXXXX /XXXXX
\XXXXXXXXX \ /XXXXX/
\XXXXXX > _/XXXXX/
\XXXXX--__/ __-- XXXX/
-XXXXXXXX--------------- XXXXXX-
\XXXXXXXXXXXXXXXXXXXXXXXXXX/
""VXXXXXXXXXXXXXXXXXXV""
シグニチャー取得バッチ
javap -s *.class
pause
- 結果
Compiled from "Moo.java"
public class Moo {
public Moo();
descriptor: ()V
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
public static java.lang.String[] getArray();
descriptor: ()[Ljava/lang/String;
public static java.lang.String cowSay(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
}
Windowsのみの手順
必要なパッケージの配置
- llvm-mingw
今回はCドライブ配下に配置します。
Cコンパイルのバッチ
chcp 65001
set JAVA_HOME=C:\Program Files\Java\jdk-24
C:\llvm-mingw-20250709-msvcrt-x86_64\bin\x86_64-w64-mingw32-gcc.exe -Wall ^
-I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" ^
-L"%JAVA_HOME%\bin\server" -ljvm ^
-lpthread ^
-o jni_exec jni_exec.c
pause
Linuxのみの手順
WSLの設定(例)
WSL2とUbuntu 24.04のインストール手順だけで日本語化まではしなくて良いです。
必要なパッケージインストール
sudo apt install gcc openjdk-21-jdk
Cコンパイル用のバッチ
chcp 65001
set command=export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64;^
export LD_LIBRARY_PATH=$JAVA_HOME/lib/server;^
gcc -Wall^
-I$JAVA_HOME/include -I$JAVA_HOME/include/linux^
-lpthread^
-o jni_exec jni_exec.c ^
-L$LD_LIBRARY_PATH -ljvm;
wsl -e ^
bash -l -c "%command%"
pause
実行手順
Windowsで実行
chcp 65001
set CLASSPATH=%~dp0lib\cowsay-1.1.0.jar
set PATH=%JAVA_HOME%\bin\server;%PATH%
jni_exec.exe
echo ERRORLEVEL: %ERRORLEVEL%
pause
WSL(Windows Subsystem for Linux)で実行
chcp 65001
set command=export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64;^
export PATH=$JAVA_HOME/bin:$PATH;^
export LD_LIBRARY_PATH=$JAVA_HOME/lib/server;^
export CLASSPATH=$(realpath $(dirname $0))/lib/cowsay-1.1.0.jar;^
./jni_exec;^
echo ERRORLEVEL: $?
wsl -e ^
bash -l -c "%command%"
pause
実行結果(例)
結果
CLASSPATH: -Djava.class.path=.:/mnt/c/path/to/jar-folder/lib/cowsay-1.1.0.jar
<VM新規作成>
<VM再利用>
<<Result from Java>>
__________________________________
< three-eyes said, Hello from JNI! >
----------------------------------
\ ^___^
\ (ooo)\_______
(___)\ )\/\
||----w |
|| ||
<VM再利用>
<<Result from Java>>
___________________________________
< supermilker said, Hello from JNI! >
-----------------------------------
\ ^__^
\ (oo)\_______ ________
(__)\ )\/\ |Super |
||----W | |Milker|
|| UDDDDDDDDD|______|
<VM再利用>
<<Result from Java>>
_____________________________
< udder said, Hello from JNI! >
-----------------------------
\
\ (__)
o o\
('') \---------
\ \
| |\
||---( )_|| *
|| UU ||
== ==
<VM再利用>
<<Result from Java>>
_____________________________________
< flaming-sheep said, Hello from JNI! >
-------------------------------------
\ . . .
\ . . . ` ,
\ .; . : .' : : : .
\ i..`: i` i.i.,i i .
\ `,--.|i |i|ii|ii|i:
UooU\.'@@@@@@`.||'
\__/(@@@@@@@@@@)'
(@@@@@@@@)
`YY~~~~YY'
|| ||
<VM再利用>
<<Result from Java>>
___________________________________
< vader-koala said, Hello from JNI! >
-----------------------------------
\
\ .
.---. //
Y|o o|Y//
/_(i=i)K/
~()~*~()~
(_)-(_)
Darth
Vader
koala
<VM再利用>
<<Result from Java>>
___________________________________
< stegosaurus said, Hello from JNI! >
-----------------------------------
\ . .
\ / `. .' "
\ .---. < > < > .---.
\ | \ \ - ~ ~ - / / |
_____ ..-~ ~-..-~
| | \~~~\.' `./~~~/
--------- \__/ \__/
.' O \ / / \ "
(_____, `._.' | } \/~~~/
`----. / } | / \__/
`-. | / | / `. ,~~|
~-.__| /_ - ~ ^| /- _ `..-'
| / | / ~-. `-. _ _ _
|_____| |_____| ~ - . _ _ _ _ _>
<VM再利用>
<<Result from Java>>
___________________________
< ren said, Hello from JNI! >
---------------------------
\
\
____
/# /_\_
| |/o\o\
| \\_/_/
/ |_ |
| ||\_ ~|
| ||| \/
| |||_
\// |
|| |
||_ \
\_| o|
/\___/
/ ||||__
(___)_)
<VM再利用>
<<Result from Java>>
_________________________________
< bud-frogs said, Hello from JNI! >
---------------------------------
\
\
oO)-. .-(Oo
/__ _\ /_ __\
\ \( | ()~() | )/ /
\__|\ | (-___-) | /|__/
' '--' ==`-'== '--' '
<VM再利用>
<<Result from Java>>
__________________________________
< three-eyes said, Hello from JNI! >
----------------------------------
\ ^___^
\ (ooo)\_______
(___)\ )\/\
||----w |
|| ||
<VM再利用>
<<Result from Java>>
___________________________
< ren said, Hello from JNI! >
---------------------------
\
\
____
/# /_\_
| |/o\o\
| \\_/_/
/ |_ |
| ||\_ ~|
| ||| \/
| |||_
\// |
|| |
||_ \
\_| o|
/\___/
/ ||||__
(___)_)
ERRORLEVEL: 0
まとめ
・かなり些細なことでも分かり辛いエラーを起こすので、非常に大変でした。
・例えば、Windows向けのllvm-mingwとLinux向けのGNU GCCの挙動が異なり、
-L"%JAVA_HOME%\bin\server" -ljvm
を末尾に書かないと
JNI_GetCreatedJavaVMsとJNI_CreateJavaVMが見つからないなど...
・環境変数LD_LIBRARY_PATHを書いて、sudo ldconfig -p | grep libjvm
でも
登録済みを確認できるのに、jni.hの2つの関数だけが見つからないという
不思議な事象の解決には、6時間以上かかりました。