Как я к яве подключался

После знакомства с Java сразу возникает мысль - а нельзя ли воспользоваться всей этой кучей полезных возможностей без необходимости программировать на данном шаманском языке. Оказывается можно. Ява разделяется на три составляющие - 1) Java Virtual Machine; 2) стандартные библиотеки; 3) ну и собственно язык ява. JVM это обычная dll'ка и ее можно запустить из обычной программы. После этого ей можно скормить скомпилированную ява-программу. Или - получить доступ к возможностям стандартных библиотек. Правда дело это муторное и без меры запутанное.

Подготовка к приступу

Что надо для работы? Во-первых spf4 . Во-вторых ява. Ява бывает разной. Микрософтовская и сановская явы используют разные механизмы для взаимодействия с нативными программами. Я пока разобрался только с сановской явой. Значит нужно взять Java 2 (я эксперементировал с версией 1.3.1). В нее встроен механизм Java Native Interface версии 1.2. С ним и будем работать. Да яву еще надо установить - переменная PATH должна указывать на \jre\bin и на \jre\bin\classic (здесь собственно jvm и обитает).

Первым делом надо перевести jni.h в форто-понятный вид. В этом заголовочном файле содержатся описания двух структур: JNIEnv и JavaVM (ну и еше по мелочи). Эти структуры набиты членами-функциями. Раньше мне такие функции не попадались, пришлось нарисовать слово для их описания:

: f: ( offset "new-name" -- offset+cell )
  CREATE
  DUP , CELL+
  DOES> @ OVER @ + @ API-CALL
;
Здесь можно взять готовый jni.h.f и набор прочих библиотек (пригодятся).

Загрузка и запуск .class файла

Перенесем на форт пример из jni-туториала, запускающий на выполнение файл Prog.java

public class Prog {
    public static void main(String[] args) {
         System.out.println("Hello World " + args[0]);
    }
}
Прежде всего надо подумать об обработке ошибок. После вызова каждой функции из JNIEnv, невредно проверить - нормально ли она отработала. Все функции, которые нам понадобятся, возвращают ноль в случае возникновения ошибки. Это и будем проверять:
: CheckRes ( n -- n )
  DUP 0= IF DROP DestroyJVM THEN
;
Что делать с ошибками? Так как программа экспериментальная, то просто выведем на печать информацию об ошибке, и закончим работу
: DestroyJVM ( -- )
  env @ JNIEnv::ExceptionOccurred IF
    env @ JNIEnv::ExceptionDescribe DROP
  THEN
  jvm @ JavaVM::DestroyJavaVM DROP
  BYE
;
Теперь можно запускать ява-машинку
\ указатель на структуру JNIEnv. Через него идет все взаимодействие с jvm
VARIABLE env
\ указатель на экземпляр явы
VARIABLE jvm
CREATE vm_args JavaVMInitArgs::/SIZE ALLOT
CREATE options JavaVMOption::/SIZE ALLOT
\ сообщаем яве где искать Prog.class (в текущем каталоге)
S" -Djava.class.path=." DROP options JavaVMOption::optionString !
0x00010002 vm_args JavaVMInitArgs::version !
options    vm_args JavaVMInitArgs::options !
1          vm_args JavaVMInitArgs::nOptions !
JNI_TRUE   vm_args JavaVMInitArgs::ignoreUnrecognized !

\ получаем jvm
vm_args env jvm JNI_CreateJavaVM 0< IF
   S" Can't create Java VM" H-STDERR
   ?DUP IF WRITE-FILE THROW ELSE 2DROP THEN
   1 HALT
THEN
Теперь начинаются шаманские пляски. Интерфейс взаимодействия с jvm очень развесистый (более 200 функций) и запутанный. Дело в том что создатели явы очень гордятся ее строгой типизацией и объектностью, и поэтому старательно охраняют ее от всяческих ошибок и проблем внешнего мира. Для того чтобы добраться до чего-нибудь надо точно знать - что из себя представляет это что-нибудь. Есть свои функции для работы с классами, объектами, методами классов, полями классов; эти методы могут быть статическими или нет; могут возвращать значения разных типов. И все это надо учитывать. В общем открываем доку и начинаем меедленно двигаться
\ ищем класс, с которым будем работать
S" Prog" DROP
 env @ JNIEnv::FindClass         CheckRes TO cls
\ получаем id нужного метода
S" ([Ljava/lang/String;)V" DROP S" main" DROP cls
 env @ JNIEnv::GetStaticMethodID CheckRes TO mid
Вот пример - если бы main был не static - то надо было бы использовать GetMethodID. А если бы main был полем - то GetStaticFieldID. Причем мало знать имя искомого метода - нужна еще его сигнатура. В данном случае это ([Ljava/lang/String;)V. Что означает - метод возвращает значение типа void, а в качестве параметров принимает массив объектов типа строка.
\ создаем новую строку
S" from FORTH!" DROP
 env @ JNIEnv::NewStringUTF      CheckRes TO jstr
\ main требует массив, значит делаем из строки массив
S" java/lang/String" DROP
 env @ JNIEnv::FindClass         CheckRes TO stringClass
jstr stringClass 1
 env @ JNIEnv::NewObjectArray    CheckRes TO args
\ и наконец вызываем метод main (mid) в классе Prog (cls)
\ с входным параметром args.
args mid cls
 env @ JNIEnv::CallStaticVoidMethod DROP
\ уничтожаем нашу ява-машинку
DestroyJVM
Результатом работы будет печать строки "Hello World from FORTH!" в консоли. Итоговый файл proba1.f.

Доступ к возможностям ява-библиотек

Теперь добьемся того же результата без использованя ява-кода:

REQUIRE JNIEnv       ~af\jni\jni.h.f
REQUIRE SLITERAL2    ~af\lib\sliteral2.f
REQUIRE STR@         ~ac\lib\str2.f
REQUIRE USES         ~af\lib\api-func.f

USES jvm.dll

0 VALUE env
0 VALUE jvm

: CreateJVM ( -- )
  { \ *env *jvm [ JavaVMInitArgs::/SIZE ] vm_args -- }
  \ никаких опций передовать jvm не будем
  0x00010002 vm_args JavaVMInitArgs::version !
  0          vm_args JavaVMInitArgs::nOptions !
  JNI_TRUE   vm_args JavaVMInitArgs::ignoreUnrecognized !

  vm_args AT *env AT *jvm JNI_CreateJavaVM 0< IF
     S" Can't create Java VM" H-STDERR
     ?DUP IF WRITE-FILE THROW ELSE 2DROP THEN
     1 HALT
  THEN
  \ упростим последующий код - уберем @ после каждого env
  *env TO env
  *jvm TO jvm
;
: DestroyJVM ( -- )
  env JNIEnv::ExceptionOccurred IF
    env JNIEnv::ExceptionDescribe DROP
  THEN
  jvm JavaVM::DestroyJavaVM DROP
  BYE
;
: CheckRes ( n -- n )
  DUP 0= IF DROP DestroyJVM THEN
;

: Z" POSTPONE S" DROP ; IMMEDIATE

CreateJVM
  \ Мы хотим напечатать строку. На яве это звучит так -
  \ System.out.println("Hello World");
  \ Смотрим в доке - что такое System. Оказывается это клас java.lang.System
  \ Получим ссылку на него. Только вместо . надо почему-то писать /
  Z" java/lang/System"
   env JNIEnv::FindClass        CheckRes VALUE cls
  \ Ищем out. Это статическое поле типа PrintStream. Ищем в доке PrintStream
  Z" Ljava/io/PrintStream;" Z" out" cls
   env JNIEnv::GetStaticFieldID CheckRes VALUE fid
  \ Получили id out-та. Теперь надо получить сам out
  fid cls
   env JNIEnv::GetStaticObjectField CheckRes VALUE joout
  \ out - это объект. А объект это вам не класс. Надо из объекта
  \ получить его класс (PrintStream)
  joout
   env JNIEnv::GetObjectClass   CheckRes VALUE clsout
  \ создаем строку, которую надо напечатать
  Z" FORTH + JAVA = IT'S WORK!"
   env JNIEnv::NewStringUTF     CheckRes
  \ ищем в классе PrintStream метод println, умеющий печатать строки
  Z" (Ljava/lang/String;)V" Z" println" clsout
   env JNIEnv::GetMethodID      CheckRes
  \ наконец печатаем (только надо не забыть, что печатает объект out, а
  \ не класс в котором мы только что нашли метод для печати)
  joout
   env JNIEnv::CallVoidMethod   CheckRes DROP
DestroyJVM