Java extensioner med nativelib-filer

Intro

Hur gör man om man vill distribuera ett Java-program som använder en extension som är beroende av nativelib-filer, det vill säga en extension som inte är gjord med enbart ren Java? Man kan i huvudsak göra på följande sätt:

  1. Användaren får själv först hämta och installera den extension som behövs.
  2. Programmet är gjort så att det har extensionen inbakad eller startar en installation av extensionen automatisk.

Vi ska här titta närmare på det mer användarvänliga fallet (2) och använda Java 3D version 1.3.1 för Win32 som en generaliserbar fallstudie. Allt i detta dokument har testats på:

Notera att vi här enbart fokuserar på ett operativsystem men att man lätt kan få allt att fungera på flera plattformar samtidigt.

Om man skulle hämta Java 3D här och installerat detta paket så skulle filerna läggas enligt:

På mitt system skulle de ligga här:

Vi antar i detta dokument att vi arbetar utan en installation. För att kunna kompilera och köra program baserade på Java 3D utan en installation måste man ha följande extension-filer i katalogen som man utvecklar i:

När man kompilerar och utvecklar så måste man sätta classpath till dessa:

set classpath=j3daudio.jar;j3dcore.jar;j3dutils.jar;vecmath.jar;.

Notera att DLL-filerna inte är med i classpath, men de måste ligga i samma katalog som vi har vårt eget program i. En annan variant är att packa upp alla JAR-filer i samma katalog och då slippa använda classpath.

Fristående applikation

Vi ska först titta på hur man kan packa ihop allt i en enda (klick-startbar) JAR-fil. Detta hade varit lätt om vi inte hade haft DLL-filer men nu måste vi ta till lite knep. Den här metoden kräver endast att användaren har JDK installerad.

Lägga till följande import-rader till Tellus.java:

import java.io.*;
import java.net.*;

Lägga till följande anrop överst i konstruktorn till Tellus.java:

getDLL("J3DUtils.dll");
getDLL("j3daudio.dll");
getDLL("J3D.dll");

Lägga till följande metod till Tellus.java:

private void getDLL(String dll) {
   InputStream is = getClass().getClassLoader().getResourceAsStream(dll);

   try {
      BufferedInputStream bin = new BufferedInputStream(is);
      BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dll));
      int b = -1;
      while((b = bin.read()) != -1) { 
         bos.write(b);
      }
      bin.close();
      is.close();
      bos.flush();
      bos.close();
   } catch(Exception e) {
      System.out.println(e);
   }
}

Genom detta knep hämtar vi ut DLL-filerna från JAR-filen och lägger dem i samma katalog som JAR-filen. Man måste göra det i början av körningen eftersom DLL-filerna sedan används.

Man måste även ändra följande rad i metoden makeTellus:

TextureLoader tl = new TextureLoader("tellus.jpg", "RGB", this);

till:

URL url = getClass().getClassLoader().getResource("tellus.jpg");
TextureLoader tl = new TextureLoader(url, "RGB", this);

så att man når filen tellus.jpg från JAR-fil en. Vi har nu fått en ny:

Tellus.java

Kompilera: javac Tellus.java

Packa sen upp alla JAR-filer:

Skapa en manifest-fil manifest_tellus.txt med följande innehåll:

Main-Class: Tellus

Skapa JAR-filen:

jar cmf manifest_tellus.txt tellus.jar Tellus.class tellus.jpg J3D.dll j3daudio.dll J3DUtils.dll com javax

Nu är allt färdigt för leverans i en enda (klick-startbar) JAR-fil:

tellus.jar (3.35 MByte)

Vill man testa detta så går det givetvis även om man har Java 3D installerad men då förlorar exemplet sin tjusning :-).

Notera:

Man kan även tänka sig att lägga till funktionalitet som gör att DLL-filerna tas bort när användaren avslutar programmet. Detta går inte att göra direkt i samma tråd eftersom filerna är inladdade och låsta av operativsystemet. En lösning är att starta ett nytt program av Tellus men med ett argument när man avslutar huvudprogrammet:

Runtime.getRuntime().exec("java -jar Tellus.jar removeDLLs");
System.exit(-1);

Sen kan man exempelvis ha följande i main-metoden:

if(args.length > 0) {
   try { Thread.sleep(1000); } catch(Exception e) {}
   removeDLLs();
} else {
   new Tellus();
}

Här är removeDLLs-metoden:

private static void removeDLLs() {
   removeDLL("J3DUtils.dll");
   removeDLL("j3daudio.dll");
   removeDLL("J3D.dll");
}

private static void removeDLL(String dll) {
   File file = new File(dll);
   boolean success = file.delete();
}

Då fungerar även detta.

Web Start

Om vi vill starta ett program direkt från Internet kan man använda Web Start. Den här metoden kräver generellt enbart att användaren har JDK installerad men för Win32 finns ett sätt att från en browser automatiskt installera även Web Start (se vidare här).

Lägg till följande import-rader till Tellus.java:

import java.net.*;

Man måste även ändra följande rad i metoden makeTellus:

TextureLoader tl = new TextureLoader("tellus.jpg", "RGB", this);

till:

URL url = getClass().getClassLoader().getResource("tellus.jpg");
TextureLoader tl = new TextureLoader(url, "RGB", this);

så att man når filen tellus.jpg från JAR-fil en. Vi har nu fått en ny:

Tellus.java

Kompilera: javac Tellus.java

Man måste man signera alla ingånde filer. Skapa först en JAR-fil för Tellus.java:

jar cf tellus.jar Tellus.class tellus.jpg

Gör JAR-filer av DLL-filerna:

Signera alla JAR-filer:

Skapa en JNLP-fil tellus.jnlp:

<?xml version="1.0" encoding="utf-8"?> 
<jnlp spec="1.0+" codebase="http://atlas.dsv.su.se/~adapt/im/t_nativelibs/web_start" href="tellus.jnlp">
  <information>
    <title>Tellus Web Start</title>
    <vendor>Pierre A. I. Wijkman</vendor>
    <homepage href="http://www.dsv.su.se/~pierre/"/>
    <description>Demo of Web Start</description>
    <description kind="short">A simple demo of Web Start.</description>
    <offline-allowed/>
  </information>
  <security>
    <all-permissions/>
  </security>
  <resources>
    <j2se version="1.4"/>
    <jar href="tellus_signed.jar"/>
    <jar href="j3daudio_signed.jar"/>
    <jar href="j3dcore_signed.jar"/>
    <jar href="j3dutils_signed.jar"/>
    <jar href="vecmath_signed.jar"/>
    <nativelib href="j3dDLL_signed.jar"/>
    <nativelib href="j3daudioDLL_signed.jar"/>
    <nativelib href="j3dutilsDLL_signed.jar"/>
  </resources>
  <application-desc main-class="Tellus"/> 
</jnlp>

Vill man testa lokalt med en JNLP-fil tellus_web_start_local.jnlp så behöver man bara ändra på jnlp-taggen:

<jnlp spec="1.0+" codebase="file:." href="tellus_local.jnlp">

Notera:

Lägg sen ut alla signerade JAR-filer och JNLP-filen och man kan köra direkt från JNLP-filen:

tellus.jnlp (alla signerade JAR-filer är tillsammans på 5.60 MByte, tellus.jnlp är på 993 Byte)

Vill man testa detta så går det givetvis även om man har Java 3D installerad men då förlorar exemplet sin tjusning :-).

Äpplen

Om vi vill starta ett program direkt från Internet kan man även använda äpplen. Ska man göra så här avancerade saker så kan man givetvis inte använda browserns eventuella interna Java-tolk utan man måste använda Java Plug-In (och eventuellt HTMLConverter). Den här metoden kräver inte att användaren har JDK installerad eftersom även den installationen kan göras automatiskt när en browser upptäcker att en HTML-sida använder Java Plug-In.

Lägga till följande import-rader till Tellus.java:

import java.applet.*;
import java.net.*;
import java.io.*;

Gör om koden till ett äpple:

public class Tellus extends Applet {

och använd init() istället för main och konstruktorn:

public void init() {
   Canvas3D canvas3D = new Canvas3D((new GraphicsConfigTemplate3D()).getBestConfiguration(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0].getConfigurations())); 
   SimpleUniverse su = new SimpleUniverse(canvas3D);
   su.getViewingPlatform().setNominalViewingTransform();
   su.addBranchGraph(makeBranchGraph());
   setLayout(new BorderLayout());
   add(canvas3D, BorderLayout.CENTER);
}

Man måste även ändra följande rad i metoden makeTellus:

TextureLoader tl = new TextureLoader("tellus.jpg", "RGB", this);

till:

URL url = getClass().getClassLoader().getResource("tellus.jpg");
TextureLoader tl = new TextureLoader(url, "RGB", this);

så att man når filen tellus.jpg från JAR-fil en. Vi har nu fått en ny:

Tellus.java

Kompilera: javac Tellus.java

Tellus måste packas i en JAR-fil med en speciell manifest-fil manifest_tellus.txt som visar att äpplet använder en extension:

Manifest-Version: 1.0
Extension-List: j3dcore
j3dcore-Extension-Name: javax.media.j3d
j3dcore-Implementation-Vendor-Id: com.sun
j3dcore-Implementation-Version: 1.3.0
j3dcore-Implementation-Title: Java 3D Core Runtime Environment
j3dcore-Specification-Title: Java 3D API Specification
j3dcore-Specification-Version: 1.3
j3dcore-Specification-Vendor: Sun Microsystems, Inc.
j3dcore-Implementation-URL: http://www.dsv.su.se/~adapt/im/t_nativelibs/applet/loader_signed.jar

Eftersom äpplet använder en extension som innehåller DLL-filer så måste vi under j3dcore-Implementation-URL ange en URL som pekar på en signerad JAR-fil som laddar ner extensionen. Här kan man använda konstruktionen:

fileName-$(os-name)$.jar

om man vill använda olika JAR-filer för olika operativsystem, se här för mer info om det.

Skapa JAR-filen:

jar cmf manifest_tellus.txt tellus.jar Tellus.class tellus.jpg

Gör en HTML-fil tellus.html som pekar på äpplet, här är applet-taggen:

<!--"CONVERTED_APPLET"-->
<!-- HTML CONVERTER -->
<OBJECT classid = "clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" codebase = "http://java.sun.com/products/plugin/autodl/jinstall-1_4-windows-i586.cab#Version=1,4,0,0" WIDTH = "256" HEIGHT = "256" >
<PARAM name="progressbar" value="true">
<PARAM NAME = CODE VALUE = "Tellus.class" >
<PARAM NAME = ARCHIVE VALUE = "tellus.jar" >
<PARAM NAME = "type" VALUE = "application/x-java-applet;version=1.4">
<PARAM NAME = "scriptable" VALUE = "false">
<COMMENT>
<EMBED type = "application/x-java-applet;version=1.4" CODE = "Tellus.class" ARCHIVE = "tellus.jar" WIDTH = "256" HEIGHT = "256" scriptable = false pluginspage = "http://java.sun.com/products/plugin/index.html#download">
>NOEMBED>
</NOEMBED>
</EMBED>
</COMMENT>
</OBJECT>
<!--
<APPLET CODE = "Tellus.class" ARCHIVE = "tellus.jar" WIDTH = "256" HEIGHT = "256">
</APPLET>
-->
<!--"END_CONVERTED_APPLET"-->

Vi gör sen klassen som laddar ner alla filer som vi behöver: Loader.java

Kompilera: javac Loader.java

Loader måste packas i en signerad JAR-fil och använda en speciell manifest-fil manifest_loader.txt:

Manifest-Version: 1.0
Extension-Name: javax.media.j3d
Specification-Version: 1.3
Specification-Vendor: Sun Microsystems, Inc.
Implementation-Version: 1.3.0
Implementation-Vendor: Sun Microsystems, Inc.
Implementation-Vendor-Id: com.sun
Main-Class: Loader

Gör en JAR-fil:

jar cmf manifest_loader.txt loader.jar Loader.class j3daudio.jar j3dcore.jar j3dutils.jar vecmath.jar J3D.dll j3daudio.dll J3DUtils.dll

Signera JAR-filen:

Lägg sen ut tellus.html, tellus.jar, loader_signed.jar på Internet och kör direkt från:

tellus.html (loader_signed.jar är på 2.87 MByte, tellus.jar är på 55.1 KByte)

Vill man testa detta så går det givetvis även om man har Java 3D installerad men då förlorar exemplet sin tjusning :-). Notera att man får Java 3D installerad efter denna test.

Fördelen med detta sätt att köra äpplet är att en användare enbart behöver ladda ner extensionen första gången. Nackdelen är att exensionsfilerna måste sparas till <java.home>/lib/ext och om användaren inte har administratörsrättigheter så går inte det. Vill man komma ifrån detta måste man troligtvis göra på ett helt annat sätt och använda en egen klass-laddare med mera.