· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
GCJ

  • Gnu Compiler for Java의 약자. java로 씌여진 코드를 Native Code로 바꿔주는 컴파일러

1. 기본적인 사용

1.1. 사용환경

GCJ는 gcc의 일부이기 때문에 gcc를 사용할 수 있는 환경이 되어야 한다. 기본적으로 리눅스에서는 사용이 가능하며 윈도우즈에서 http://www.mingw.org 에서 제공하는 것을 이용할 수 있다.

1.2. 간단한 예제

public class ExamGCJ {
         public static void main(String[] args) {
                  System.out.println("ExamGCJ Class GCJ Test");
         }
}
위와 같은 예제를 가지고 해보자. 먼저 위와 같은 파일을 하나 작성하고 파일 이름을 ExamGCJ.java로 해서 저장한다. 그리고 다음과 같은 명령줄을 통해서 GCJ로 컴파일 할 수 있다.
# gcj --main=ExamGCJ ExamGCJ.java -o ExamGCJ
이렇게 하면 ExamGCJ라는 Native Code를 담은 실행 파일이 생성된다. GCC에 익숙하다면 바로 알 수 있듯이 GCJ에서 역시 gcc와 비슷한 옵션이 사용된다.

  • -o 이름 지정
  • -c object code만 생성

1.3. 두개 이상의 클래스로 이루어진 코드 컴파일

public class ExamGCJ {
         public static void main(String[] args) {
                  System.out.println("Two Class GCJ Test");
                  new LibExamGCJ();
         }
}

public class LibExamGCJ {
         LibExamGCJ() {
                  System.out.println("Two Class GCJ Test --- this is print in lib");
         }
}
위의 코드를 각 클래스 이름을 파일 이름으로 하여 두개의 코드 파일에 저장한다. 그리고 다음과 같은 명령으로 컴파일 할 수 있다.
# gcj -c LibExamGCJ.java
# gcj -c ExamGCJ.java
# gcj --main=ExamGCJ ExamGCJ.o LibExamGCJ.o -o ExamGCJ
위에서부터 자꾸 보이겠지만 --main이라는 옵션이 있다. 이쯤 되었으면 이 옵션이 무엇을 의미하는지 이해했으리라 생각한다 바로 main이라는 메소드가 있는 클래스의 이름을 지정하는 것이다. 이를 지정해주지 않고 컴파일을 해보기 바란다. 분명 에러를 출력할 것이다.

1.4. 바이트코드로부터 컴파일

컴파일 할 때 .java 대신 .class 파일을 넣어주면 컴파일된 바이트 코드로부터 컴파일이 가능하다.
# gcj -c LibExamGCJ.class
이것이 왜 필요할까? 현재 gcj는 java 5의 몇가지 향상된 구문을 인식하지 못한다.
ForeachExam.java:16: error: Invalid declaration.
                for(File f : fileList){
                              ^
이때 jdk 1.5의 javac로 먼저 바이트코드로 변환한 뒤 gcj에서 컴파일하면 문제없이 사용이 가능하다.

1.5. 라이브러리로 묶어서 사용하기

분명 어떤 언어로 프로그래밍을 하든지 자주 쓰는 코드를 묶어두고 사용하는 것이 편리하다. GCJ에서도 자주 사용하는 코드를 묶어두고 필요할때 사용하는 방법이 존재한다. GCJ는 GCC의 일부다. 따라서 GCC처럼 라이브러리를 생성할 수 있다.

필자는 리눅스를 자주 사용하지 않는다. 따라서 여기의 설명이 윈도우즈의 MinGW에서의 작업에 초점이 맞추어져 있다는 것을 참고하기 바란다.
# ar -crs libexam.a LibExamGCJ.o
이렇게 하면 이미 생성한 오브젝트 코드를 라이브러리로 포함시킨다. 오브젝트 코드의 생성은 앞에서 설명을 했기에 생략한다.

아무튼 이렇게 생성한 것을 같이 링크하면 되는데 그것역시 GCC와 동일하다.
# gcj --main=ExamGCJ ExamGCJ.o -L.\ -lexam
대략 이런식이라고 적어두면 이해를 할 수 있을 것이다. ExamGCJLibExamGCJ라는 두개의 클래스는 임의로 만들어 테스트해보기 바란다. 하지만 이런식으로 할 때 한가지 문제는 ExamGCJ.o를 생성하기 위해 GCJ로 빌드할 때 클래스의 형태를 알아야.. LibExamGCJ라는 클래스를 사용할 때 에러를 체크하는데 있어서 문제가 되지 않는다.

따라서 LibExamGCJ와 같이 라이브러리로 묶을 소스 코드는 바이트 코드로도 생성해서 Jar로 묶어두고 이 라이브러리를 사용하는 경우에 클래스 패스에 포함시키는 것이 좋다.
# gcj -C LibExamGCJ.java
# jar cf swt.jar LibExamGCJ.class
이런 방법으로 개발을 좀더 편하게 할 수 있다. 그리고 이미 존재하는 패키지를 빌드해서 사용하는데 유용하다.

2. 패키지를 라이브러리화 하기

GCJ는 주로 Native Code를 생성하기 위해 사용하는 컴파일러이기 때문에 자바로 이루어진 패키지를 라이브러리화하여 링크하는 것이 원할한 개발을 위해 바람직하다. 특히 링크할때 오브젝트코드(.o)는 사용하지 않아도 같이 묶지만 정적링크라이브러리(.a)는 필요한 것만 묶어지므로 라이브러리화 해서 사용하는 것이 좋다. 그러면 그 방법을 하나의 예제와 함께 간단히 소개하고자 한다.

2.1. 패키지 안의 오브젝트코드의 이름은 어떻게 정해지는가?

패키지의 구성을 보면 보통 이런 형태로 되어 있을 것이다.

java.util.Vector

위에 보면 알 수 있듯이 java.util 패키지의 Vector클래스이다. 그리고 소스 코드 내에서 사용하려면 다음처럼 import를 한다.

import java.util.Vector

물론 GCJ로 컴파일 할 소스도 똑같이 한다. 그럼 이것을 사용한 코드를 링크할때는 어떤 이름을 가진 오프젝트가 필요한가? 바로 다음과 같은 이름을 가진 오브젝트 코드가 필요하게 된다. 라이브러리 안에서 이야기이다.

java_util_Vector.o

그리고 실제로 libgcj.a안에 존재한다. 아무튼 그렇게 라이브러리를 만들어두고 필요할때마다... 즉 해당 패키지를 사용할때마다 오프젝트 코드가 아닌 라이브러리를 링크해서 사용하면 된다.

2.2. 간단한 예제를 통해서 이해하기

이제 간단한 예제를 통해서 이해해 보자. 이쯤되면 앞의 "GCJ의 간단한 소개"에서는 사용하지 않았지만 Makefile을 사용하는게 편할 것이다. 물론 GCC를 사용하시는 분들은 당연히 받아들여지겠지만 JAVA만 하시던 분들은 조금 어색하실지도 모른다. 보통 자바로 프로그래밍을 하면 Apache Project의 Ant가 그 비슷한 역할을 하기 때문에 Ant가 익숙할 것이다.

아무튼 이제 우리는 GCC(Gnu Compiler Collection)을 사용하니 Gnu Make Tool인 Makefile을 사용할 것이다. 그리고 Makefile의 복잡한 것은 모두 제외하고 꼭 필요한 단순한 것만 사용할 것이므로 걱정하지 않아도 된다. 혹시 Makefile에 대한 내용을 모르신다면 kldp.org에서 찾아서 가볍게 읽어보기 바란다.

그럼 이제 시작하도록 하겠다. 먼저 디렉토리 구성을 간단히 살펴보자. 존재하는 디렉토리는 다음과 같다.

./bin
./obj
./lib
./src
이렇게 네개인데... ./src 디렉토리 아래 소스만 직접 작성을 하고 나머지 디렉토리는... Make를 하면서 결과물이 필요에 따라 저장될 디렉토리이다. 그리고 ./src디렉토리 아래에는...

./src/org/nahome/hangulee/
이런 디렉토리가 존재한다. 그리고 hangulee디렉토리 아래 LibExamGCJ.java, LibExamGCJ2.java라는 파일이 존재한다. 그리고 각 파일의 소스 코드는 간단한 예제에 맞게 생성자만 존재한다. 다음과 같다.

package org.nahome.hangulee;
public class LibExamGCJ
{
  LibExamGCJ()
  {
    System.out.println("Two Class GCJ Test --- this is print in lib");
  }
}

package org.nahome.hangulee;
public class LibExamGCJ2
{
  LibExamGCJ2()
  {
    System.out.println("Two Class GCJ Test --- this is print in lib2");
  }
}
다음은 실제 우리가 예에서 사용할 Makefile이다. Makefile은 상위.. 그러니까 ./src 디렉토리가 있는 디렉토리에 존재해야 한다.

CLASSPATH = ./src;

OBJS = \
  obj/org_nahome_hangulee_LibExamGCJ.o \
  obj/org_nahome_hangulee_LibExamGCJ2.o

CLASSES = \
  bin/org/nahome/hangulee/LibExamGCJ.class \
  bin/org/nahome/hangulee/LibExamGCJ2.class

all:$(OBJS) $(CLASSES) exam.jar lib/libexam.a

lib/libexam.a:
        ar -crs ./lib/libexam.a ./obj/*.o

exam.jar:
        jar cf exam.jar -C bin .

bin/org/nahome/hangulee/LibExamGCJ.class:src/org/nahome/hangulee/LibExamGCJ.java
        gcj -C -fCLASSPATH=$(CLASSPATH) -d bin src/org/nahome/hangulee/LibExamGCJ.java

obj/org_nahome_hangulee_LibExamGCJ.o:src/org/nahome/hangulee/LibExamGCJ.java
        gcj --classpath=$(CLASSPATH) -c src/org/nahome/hangulee/LibExamGCJ.java \
        -o obj/org_nahome_hangulee_LibExamGCJ.o

bin/org/nahome/hangulee/LibExamGCJ2.class:src/org/nahome/hangulee/LibExamGCJ2.java
        gcj -C -fCLASSPATH=$(CLASSPATH) -d bin src/org/nahome/hangulee/LibExamGCJ2.java

obj/org_nahome_hangulee_LibExamGCJ2.o:src/org/nahome/hangulee/LibExamGCJ2.java
        gcj --classpath=$(CLASSPATH) -c src/org/nahome/hangulee/LibExamGCJ2.java \
        -o obj/org_nahome_hangulee_LibExamGCJ2.o
이렇게 준비가 되었다면 make해보시기 바란다. make하면 아시겠지만 ./obj 아래에 오브젝트 파일이 생성된다. 그리고 ./lib 아래에 libexam.a가 생성되고 상위에 exam.jar이 생성될 것이다. exam.jar은 이 패키지를 사용할 소스 코드를 컴파일 할때 클래스 패스에 지정하기 위해 필요하다.

자!.. 이제 간단한 라이브러리가 준비되었다.

2.3. 라이브러리를 사용한 간단한 예제

예제는 다른 디렉토리에다가 만들도록 할 것이다. 그리고 이번에는 Makefile없이 간단히 컴파일 하는 것으로 끝내도록 할것이다.. 우선 다음과 같은 파일을 만든다.

import org.nahome.hangulee.*;
public class ExamGCJ
{
  public static void main(String[] args)
  {
    System.out.println("Two Class GCJ Test");
    new LibExamGCJ();
    new LibExamGCJ2();
  }
}
이렇게 하고 다음처럼 빌드한다.

gcj --classpath=(위 예제가 있는 디렉토리 경로)/exam.jar -c ExamGCJ.java -o ExamGCJ.o
gcj --main=ExamGCJ ExamGCJ.o -L(위 예제가 있는 디렉토리 경로) -lexam -o ExamGCJ
이렇게 하면 ExamGCJ라는 Native Code가 담긴 파일이 생성된다. 이것을 실행하면 결과는...

Two Class GCJ Test
Two Class GCJ Test --- this is print in lib
Two Class GCJ Test --- this is print in lib2
이렇게 나올것이다.... 이제 이해가 되는가? 물론 위에서 클래스 패스를 이렇게 지정해도 된다.

(위 예제가 있는 디렉토리 경로)/src
하지만 이렇게 하면 라이브러리를 배포할때 어려움이 따를 것이다. 그러니 jar을 생성하는게 편리하다. 자!.. 이제 더 낳은 프로그램을 만들 수 있는 준비가 되었는가? 또 다른 패키지를 라이브러리로 만들어보고 싶지 않은가?

3. Class.forName를 사용할때

JAVA로 프로그래밍을 할때 JDBC 드라이버를 동적으로 로딩하는 경우... 그 경우 외에는 그다지 사용되지 않은듯 하는 메소드일 것이다. 하지만 데이터 베이스 없이 프로그래밍을 하는 경우도 흔치 않으니 자주 사용된다고 볼 수 도 있을 것이다. 그러면 이런 Class.forName이라는 클래스가 어떻게 동작하는지에 대해서 간단히 알아보자.

3.1. GCJ로 프로그래밍 할때 Class.forName메소드

이제 이 메소드를 GCJ에서 어떻게 사용할 수 있을까? 위에서 보니 동적으로 로딩하는데... GCJ는 하나로 링크할때 묶어서 사용되어야 한다는 것을 알게 된다. 다음 예제를 수행해보기 바란다.

public class ExamClassforName
{
  public static void main(String[] args)
  {
    System.out.println("Class.forName Example");
    try
    {
      Class ex = Class.forName("TextClass");
    }
    catch(Exception e)
    {
      System.out.println("error = "+e.toString());
    }
  }
}
코드를 보니 TestClass라는 이름의 클래스가 필요하다. 알아서 하나 만들든지... 그냥 해보든지 어떻게든 해보기 바란다. 두가지 다 해서 비교해보는 것도 좋다. 어떤 결과가 나오는가? 아마도 그냥 했을때 더 좋은 소득을 얻었을 것이다. 알아서 하나 만들었을 경우에는 그냥 무리없이 실행되고 종료된다. 하지만 없을땐 예외가 발생한다. 그 클래스가 없다고... 어디에 없다고 나오는지 유의해서 봤는가?

./와 core://에서 찾았으나 없다고 나온다. 바로 그 위치에 클래스가 존재해야 한다. 결국 이 메소드를 쓰면 단순히 println할때보다.. 네이티브 코드의 크기가 1MB정도 증가한것을 볼 수 있는데... 클래스를 해석할.. 필요한게 바인딩 된 것일 것이다. 그 아래 컴파일된 클래스가 있다면... 그런데 그 컴파일된 클래스는 바이트코드로 컴파일 되어야 한다. 아무튼 있다면 그것을 읽어들여서 사용하게 된다.

3.2. 간단한 예를 통해 알아보기

이제 예를 통해 조금 더 알아보도록 하자. 아래와 같이 한개의 인터페이스와 그것을 구현한 클래스 그리고 사용할 클래스를 준비한다.

public interface forNameExamInterface
{
  void test();
}
public class forNameExam implements forNameExamInterface
{
  forNameExam()
  {
    System.out.println("forNameExample - initial");
  }
  public void test()
  {
    System.out.println("forNameExample - test");
  }
}
public class ExamClassforName
{
  public static void main(String[] args)
  {
    System.out.println("Class.forName Example");
    try
    {
      Class ex = Class.forName("forNameExam");
      forNameExamInterface obj = (forNameExamInterface)ex.newInstance();
      obj.test();
    }
    catch(Exception e)
    {
      System.out.println("error = "+e.toString());
    }
  }
}
그리고 Makefile은 다음과 같다.

OBJS = forNameExamInterface.o \
  ExamClassforName.o
CLASSES = forNameExam.class
MAINCLASS = ExamClassforName

all: $(CLASSES) $(OBJS)
  gcj --main=$(MAINCLASS) $(OBJS) -o $(MAINCLASS)

forNameExamInterface.o : forNameExamInterface.java
  gcj -c forNameExamInterface.java -o forNameExamInterface.o

ExamClassforName.o : ExamClassforName.java
  gcj -c ExamClassforName.java -o ExamClassforName.o

forNameExam.class : forNameExam.java
  gcj -C forNameExam.java
자! make해보시기 바란다. 해보면 오프젝트 코드가 담긴 파일 두개, 네이티브 코드가 담긴 파일 하나 그리고 바이트 코드가 담긴 파일 하나가 생성된다. 실행해보면 ... 실행이 될 것이다... 이런 방법으로 사용할 수 있다. 물론 위처럼 하지 않고 바이트 코드로 생성된 forNameExam를 오브젝트 코드로 생성해서.. 하나의 실행파일에 링크할때 같이 묶을 수도 있다.

4. GCJ에서 GUI 하기

GCJ는 자바 API 1.2에 기준을 둔 API를 제공한다. 하지만 GCJ 공식 홈페이지에서 밝히고 있듯이 몇몇 패키지가 빠져 있으며 대표적인 것이 AWT이다. 따라서 GCJ로 자바 프로그래밍을 하려면 GUI에 있어서 문제를 겪게 될 수 있다. 물론 GCJ User Group의 LINK에 링크해두었듯이 XAWT같은 것도 있다.

그러한 대안으로 Eclipse를 제작하기 위해 Eclipse Project의 일부로 개발된 SWT를 GCJ로 빌드해서 사용할 수 있다. 실제 그렇게 하고 있는 사람들이 있으며 그점에 대해서는 GCJ User Group의 Projects에서 "JFace & SWT Native Library Build"를 참고하시기 바란다. 잠시 언급을 하자면 현재 이 글을 쓰고 있는 시점에 2.1을 빌드한 결과물은 타 사이트에서 베포중이며.. GCJ User Group의 Project에서는 3.0M6을 빌드했으며 이는 아직 베포하지 않고 있다. 계정 용량 관계로 마땅히 베포할 만한 장소가 없어서이다.

RefactorMe [http]JavaGnome을 이용하면 GTK/GNOME을 GUI로 사용할 수 있다. GNOME 2.6부터는 GnomePlatformBinding이라는 형태로 다른 언어로 GNOME어플리케이션을 개발하기 위한 라이브리러를 배포하는데, C++, Perl과 함께 Java가 기본으로 포함되어 있다. --iolo

4.1. SWT를 빌드하기

SWT3.0M6을 기준으로 설명하겠다. 일단 소스를 받아서 디렉토리에 모두 풀면 대략 이런 디렉토리가 나온다.

org/eclipse/swt/...

바로 패키지가 그렇게 되기 때문이다. 아무튼 이걸 염두해두고 빌드하면 된다. 패키지를 빌드하는 것은 JAVA의 Package를 라이브러리로 묶어 사용하기를 참고하기 바란다. 또한 JNI에 대한 내용도 참고하기 바란다. SWT라이브러리는 JNI를 사용하고 있기 때문이다.

하지만 그냥 빌드하면 몇가지 에러를 만나게 된다. 그런 에러를 해결하는 방법에 대해서 간단히 이야기하고 넘어가겠다. 다음 패키지의 클래스에서 에러가 발생한다.

org.eclipse.swt.custom.TableCursor
여기서는 상속받은 클래스와 리턴형만 다르게 오버라이딩 한 메쏘드인 traverse가 있어서 나오는 에러이다. 사실 이건 Java의 문법에도 맞지 않으며 에러가 발생해야 맞으나 javac로는 컴파일이 된다. 아무튼 다음처럼 바꾸면 된다.

void traverse... →boolean traverse...
물론 위처럼 해줬다면 리턴값도 정해줘야 한다는 것은 모두 알 것이다. 그리고 다음 클래스에서 에러가 발생한다.

org.eclipse.swt.custom.StyledText
위 클래스가 이미 정의되어 있다고 나온다. 그러면서 이미 정의된 위치를 알려주는데 StyledText를 알려준다. 이것은 GCJ의 버그인것으로 생각된다. 하지만 다음처럼 1차적으로 바이트 코드로 컴파일 하고 하면 된다.

javac -classpath .\src src/org/eclipse/swt/custom/StyledText.java
이 두가지만 해결하면 SWT라이브러리를 빌드할 수 있게 된다.

4.2. SWT를 사용한 간단한 예제

이제 빌드를 했다면 아마 두가지의 결과물이 있게 될 것이다.

libswt.a swt.jar

그리고 다음 예제를 준비한다.

import org.eclipse.swt.widgets.*;
public class ExamSWT
{
  public static void main(String[] args)
  {
    Display display = new Display();
    Shell shell = new Shell(display);
    shell.open ();
    while (!shell.isDisposed ())
    {
      if (!display.readAndDispatch ()) display.sleep ();
    }
    display.dispose ();
  }
}
컴파일은 어떻게 해야 하는가? 다른 기본 문서들을 읽어봤다면 알 것이다. 혹시 모르시겠으면 앞에 적었던 문서를 다시한번 참고하기 바란다. 그리고 컴파일을 한 결과를 실행하려면 다음의 두 파일이 같은 디렉토리 또는 Path에 걸린 디렉토리에 있어야 한다.

swt-awt-win32-3034.dll swt-win32-3034.dll

위 두파일은 소스를 받아서 직접 빌드를 할 수도 있으며 빌드된 것을 다운받을 수도 있다.

5. GCJ에서 Java Native Interface사용하기

자바 프로그래밍을 할때 자바 내에서 해결이 어렵거나 빠른 처리를 위해서 JNI라는 Java Native Interface를 사용한다. GCJ에서도 이와 비슷한 형태로 프로그램을 작성하는 것이 가능하다. 다음 URL에 있는 예제를 함께 살펴보면서 GCJ에서 어떻게 JNI를 사용하는지 살펴보자. http://gcc.gnu.org/java/jni-comp.txt

5.1. JNI를 위한 기초

GCJ에서는 Sun사의 Java에서처럼 JNI를 제공한다. 이를 통해서 GCJ에 패키지로 제공되지 않는다 하더라도 무엇이든지 하고자 하는 것을 하는 것이 가능하다. 하지만 JNI외에도 CNI를 제공한다. 아직 CNI에 대해서 필자는 제대로 아는 바는 없지만 어렴풋한 느낌에 의존해 이야기하자면 동적 링크 모듈의 필요성 여부이다. JNI를 사용한 경우에는 리눅스와 같은 경우에는 .so라는 모듈이 윈도우즈같은 경우에는 .dll이라는 모듈이 필요하게 된다. 반면에 CNI는 그것이 필요 없는 듯 하다. 이 점이 잘못된 내용이라면 정정해주기 바란다.

일단 이런 두가지 방법을 제공하기 때문에 GCJ에서는 기본적으로 CNI를 사용한다는 가정을 하고 자바 소스 코드를 컴파일한다. 그렇기 때문에 JNI를 사용하는 경우 그냥 컴파일을 하면 에러를 발생한다. 따라서 별도의 옵션이 필요하다. 바로 -fjni이다. 이것은 jni를 사용하는 swt를 빌드할때도 사용되었다. JNI를 사용하려는 자바 소스 코드가 있다면 다음처럼 컴파일한다.

gcj -fjni -o sample sample.java
또한 헤더 파일을 생성하는 것이 제공된다. Sun사의 Java에서는 javah라는 도구가 제공되었다. GCJ에서는 gcjh라는 도구가 제공된다. 콘솔에서 다음처럼 명령을 내려 헤더 파일을 생성하게 할 수 있다.

gcjh -jni sample
이렇게 하면 sample.h가 생성된다. 그런데 이때 생각해보아야 할것은 gcjh의 도움말을 실행해보면 알 수 있겠지만 이것은 .class파일로부터 헤더 파일을 생성한다. 따라서 이미 위와같은 경우 sample.class가 존재하고 있어야 한다.

그리고 sample.h에 맞게 구현을 하였다면 그것을 동적 링크 모듈로 컴파일해야 한다. 방법은 gcc로 프로그래밍 할때와 같다.

gcc -c sampNat.c
gcc -shared -o sampNat.dll sampNat.o
여기에 .dll로 되어 있는것은 필자가 데스크탑으로 리눅스가 아닌 윈도우즈를 쓰고 있기 때문이다. 물론 위의 URL로 가면 리눅스용 예제를 만날 수 있다. 아무튼 이쯤되면 아래 예제가 모두 이해가 될 것이다. 다시한번 언급하지만 필자는 위의 URL의 예제를 윈도우즈에서 사용할수 있도록 약간의 수정을 하였다.

5.2. 간단한 예제

간단한 예제를 보자. 위의 URL에서 그대로 복사해서 윈도우즈의 Mingw에서 사용 가능하도록 약간 수정을 했다. 거의 같다고 보아도 무방하다.

sample.java
public class sample
{
  public native void myNative(String s);

  public void myJava(String s)
  {
    s = s + ", Java";
    System.out.println(s);
  }

  public static void main(String args[])
  {
    sample x = new sample();
    x.myJava("Hello");
    x.myNative("Hello, Java (from C)");
    x.myJava("Goodbye");
  }
  
  static
  {
    System.loadLibrary("sampNat");
  }
}
예제를 보면 맨 위에 Native Method를 사용하기 위해 선언을 하였다. gcjh는 이것을 근거로 header를 생성한다. 그리고 아래 System.loadLibrary("sampNat")라고 하여 동적으로 모듈을 로딩하게 하였다.

sampNat.c
#include <jni.h>
#include "sample.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_sample_myNative (JNIEnv *env, jobject this, jstring s)
{
  jclass cls;
  jfieldID fid;
  jobject obj;
  jmethodID mid;

  printf("From C\n");

  cls = (*env)->FindClass(env, "java/lang/System");
  if (cls == 0) {
    printf("java/lang/System lookup failed\n");
    return;
  }
  fid = (*env)->GetStaticFieldID(env, cls, "out", "Ljava/io/PrintStream;");
  if (fid == 0) {
    printf("java/lang/System::out lookup failed\n");
    return;
  }
  obj = (*env)->GetStaticObjectField(env, cls, fid);
  if (obj == 0) {
    printf("GetStaticObjectField call failed\n");
    return;
  }
  cls = (*env)->GetObjectClass(env, obj);
  if (cls == 0) {
    printf("GetObjectClass(out) failed\n");
    return;
  }
  mid = (*env)->GetMethodID(env, cls, "println", "(Ljava/lang/String;)V");
  if (mid == 0) {
    printf("println method lookup failed\n");
    return;
  }
  (*env)->CallVoidMethod(env, obj, mid, s);
}
보면 알 수 있듯이 위의 sample클래스로부터 생성된 헤더를 구현하고 있다. 여기에 생성된 헤더파일은 포함하지 않았지만 생성된 모습을 열어서 확인해보기 바란다.

이렇게 했다면 빌드하는 일이 남았다. 위의 URL에서는 Makefile을 이용하고 있다. 필자는 그냥 빌드에 필요한 순서대로 나열해보도록 하겠다.

gcj -C sample.java
gcjh -jni sample
gcc -c sampNat.c -o sampNat.o
gcc -shared -o sampNat.dll sampNat.o
gcj -fjni -o sample sample.class --main=sample
첫번째 줄은 sample.java를 바이트 코드로 컴파일 하는 것이다. 다음 두번째 줄은 그 생성된 바이트 코드로부터 헤더파일을 얻는다. 세번째줄은 그것을 구현하고 있는 sampNat.c를 오브젝트 코드로 컴파일하고 있다. 그리고 네번째줄에서 동적 링크 모듈을 만들고 마지막으로 바이트 코드로 컴파일된 클래스를 네이티브 코드로 컴파일한다.

그리고 생성된 sample을 실행시켜보라. 멋지게 실행될 것이다.

6. 최적화로 성능 향상시키기

GCC의 최적화 옵션들을 GCJ에서도 그대로 사용할 수 있다.

# gcj -Os -s -march=pentium2 --main=Hello Hello.o
자세한 내용은 GccOptimizationOptions을 참고할 것.

7. 실행 파일 크기 줄이기

GCJ로 프로그램을 작성했을 때의 골칫거리 중 하나는, 최종 실행 파일의 커다란 크기이다. 필자는 SWT를 이용해서 Text 위젯에 'Hello World'를 출력하는 간단한 프로그램을 만들었는데, 그 크기는 자그마치 48.2M나 되었다. 이것은 보통의 JRE 로딩에 필요한 가비지 컬렉터와 수백개의 클래스 파일을 내장함으로써 생긴 결과로, 작은 프로그램에게는 지나치게 비대한 용량이다.

7.1. srtip을 이용한 방법

strip은 바이너리에서 불필요한 디버깅 심볼들을 제거해준다. 윈도우 유저라면 mingw/binutils가 필요한데, 필자는 [http]javacompiler 패키지에 내장된 것을 사용했다.

사용법은 매우 간단하다.

# strip Foo.exe
위에서 언급한 바이너리의 경우, 그 크기가 14.7M 정도로 줄어들었다.

7.2. gcj의 옵션을 이용한 방법

gcj로 컴파일시 -s 옵션을 주면 심볼들이 제거되고 용량이 줄어든다.

# gcj -s --main=bar.Foo *.o Foo
이 경우 용량은 위에서 strip을 사용했을 때와 동일하다. 단, 처음 오브젝트 코드를 생성시에는 -s 옵션이 불필요하다. 적용되지도 않을 뿐더러, strip을 써서 강제로 용량을 줄이면 후에 gcj에서 사용시 패키지를 찾을 수 없다는 에러가 발생한다.

# gcj -s Bar.java //-s 효과없음
# gcj -s --main=Foo *.o Foo
# strip Bar.o
# gcj -s --main=Foo *.o Foo //Bar.o 안의 패키지를 찾을 수 없다는 에러

7.3. 라이브러리 사용하기

오브젝트 코드(*.o) 대신 라이브러리(*.a)를 사용하면 사용되지 않는 클래스는 포함하지 않는다. 언급한 HelloWorld 컴파일시 용량 차이는 1MB 미만으로 매우 미미했지만, 규모가 커질 수록 도움이 될 것이다.

클래스로더 등 리플렉션을 이용하는 경우는 gcj가 모든 클래스의 사용을 감지하지 못해 실행중 에러가 발생할 수 있으므로, 주의깊게 사용하도록 한다.

7.4. libgcj.dll 만들기

gcj를 이용한 프로그램을 여럿 만드는 경우, libgcj를 별도로 모듈화하여 용량을 크게 줄일 수 있다. 다음 문서들을 참고하라.

가능하다면 java.awt.*, javax.swing.* 등의 패키지를 제거하는 것도 도움이 될 것이다.

7.5. UPX를 이용한 방법

[http]UPX를 사용하는 것도 하나의 방법이다. 압축 속도가 매우 빠르며 실행 속도에도 거의 영향을 미치지 않지만, 대신 메모리 점유율이 압축전 파일의 용량만큼 늘어난다.

사용법은 다음과 같다.
# upx -1 -o CompressedFoo.exe FatFoo.exe
숫자 옵션은 1에서 9까지로 압축률을 지정할 수 있으며 압축률이 올라갈수록 그만큼 속도도 느려진다.

다음은 앞서 언급한 프로그램을 UPX를 사용해 압축한 결과이다. 비교적 저사양인 셀400MHz에 256M 램, 퀀텀 파이어볼 6G HDD를 사용했다.

옵션 소요시간 최종 크기
-1 7초 21.1M
-5 4분 11초 17.2M
-9 11분 21초 15.4M

가장 빠른 압축에서조차 실행 파일의 크기가 절반 이하로 줄어든 것을 알 수 있다.

8. GCJ Bug & Patch

  • [http]AttachCurrentThread() not working - 시스템트레이를 자바에서 사용하려다 발견한것입니다. 현재 systray4j를 이용하는 방법과 swt의 TrayIcon패치를 사용하는 방법이 있는데 systray4j를 사용하는 방법이 이 버그때문에 잘 안되는군요 :)



문서 일지
  • 페이지 처음 만듦: 이한길
  • 패키지, Class.forName, SWT, JNI에 대해 추가. GCJ User Group을 닫으며 그대로 옮겨옴. - 이한길
  • KLDP의 서버 이상으로 손실된 자료를 개인적으로 보관하던 문서로 복원함(2006년 2월 17일) - 이한길
    (드디어 복원했습니다. 이 정도 문서면 쉽게 입문할 수 있을 거라고 생각합니다.)
  • '바이트코드로부터 컴파일', '실행 파일 압축하기' 추가(2006년 8월 27일) - netisinfinite
  • 몇가지 크기 및 최적화에 대한 내용을 더함(2006년 9월 17일) - netisinfinite


from wiki.kldp.org by me



sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2006-09-17 17:44:47
Processing time 0.0030 sec