2010年12月11日土曜日

Felix on Android でバンドルがスタートできない問題の回避方法

Android に Felix (http://felix.apache.org/) を載せて OSGi バンドルをインストールし、スタートさせると、"*** Class '${class}' was not found because bundle ${importer} does not import '${package}' even though bundle ${exporter} does export it. To resolve this issue, add an import for '${package}' to bundle ${importer}." というエラーが出てしまう。エラーメッセージの内容は、「バンドル ${exporter} がパッケージ ${package} をエクスポートしているが (= バンドル ${exporter} の MANIFEST.MF の Export-Package にパッケージ ${package} がリストされているが)、バンドル ${importer} がパッケージ ${package} をインポートしていない (= バンドル ${importer} の MANIFEST.MF の Import-Package にパッケージ ${package} がリストされていない) ため、クラス ${class} を見つけることができない。この問題を解決するためには、バンドル ${importer} でパッケージ ${package} をインポートしなさい (= バンドル ${importer} の MANIFEST.MF の Import-Package にパッケージ ${package} をリストしなさい)。」、というものである。

しかし、この問題は、バンドル ${importer} の MANIFEST.MF の Import-Package に ${package} をリストしても解決できない。Felix のコードに問題があるからだ。

【解決方法1】
本質的ではないが、てっとり早い問題解決方法は、バンドルを DEX 変換するときに --keep-classes オプションをつけること。

dx --dex --keep-classes --output=Android用バンドル.jar 元のバンドル.jar

【解決方法2】
--keep-classes オプションを付けずに DEX 変換された JAR ファイルの場合、JAR ファイルの中に元の *.class ファイル群が含まれていない。そのため、org.apache.felix.framework.ModuleImplfindClass(String) メソッド内の「bytes = contentPath[i].getEntryAsBytes(actual)」というコードで bytesnull に設定されてしまう。これにより、後続の「if (bytes != null)」条件文が false になり、「getDexFileClass((JarContent)content, name, this)」が呼び出されない。しかし、DEX 変換された JAR の場合、bytesnull かどうかはどうでもよいので、つまり、JAR ファイルの中に xxx.class というファイルが存在するかどうかはどうでもよいので(なぜなら代わりに classes.dex が含まれているので)、この if 文のチェックは不要だ。実際、bytesnull でも構わずに当該 if ブロックの中を実行するようにコードを変更すると、めでたく Felix on Android にインストールした OSGi バンドルをスタートさせることができる。


なお, getDexFileClass()null を返した場合, 後続のdefineClass()bytes == null のまま呼び出されてしまう. これを避けるため, defineClass() の呼び出しを囲っている if (clazz == null)if (clazz == null && bytes != null) にする必要がある. これをしないと, Android エミュレータで SIGSEGV エラーが発生してしまう. (NullPointerException が投げられない理由はよく分からない.)


Solutions for the problem where bundles won't start on "Felix on Android"

If an OSGi bundle is installed on Felix (http://felix.apache.org/) on Android and tried to be started, an error occurs which says "*** Class '${class}' was not found because bundle ${importer} does not import '${package}' even though bundle ${exporter} does export it. To resolve this issue, add an import for '${package}' to bundle ${importer}." This error message means "The bundle ${exporter} exports the package ${package} (= the package ${package} is listed on Export-Package in MANIFEST.MF of the bundle ${exporter}) but the bundle ${importer} does not import the package ${package} (= the package ${package} is not listed on Import-Package in MANIFEST.MF of the bundle ${importer}), so the class ${class} cannot be found. To resolve this issue, the bundle ${importer} should import the package ${package} (= the package ${package} should be listed on Import-Package in MANIFEST.MF of the bundle ${importer})."

However, this issue is not solved even after listing ${package} on Import-Package in MANIFEST.MF of the bundle ${importer}. It is because Felix code has a problem.

[ Solution 1 ]
Not essential, but a quick solution is to add --keep-classes option when you DEX-convert a bundle.

dx --dex --keep-classes --output=BundleForAndroid.jar OriginalBundle.jar

[ Solution 2 ]
If a JAR file is converted into DEX without --keep-classes option, original *.class files are not contained in a resultant JAR file. So, the code "bytes = contentPath[i].getEntryAsBytes(actual)" in findClass(String) of org.apache.felix.framework.ModuleImpl sets null to bytes. Because of this, the following conditional statement "if (bytes != null)" is evaluated as false and getDexFileClass((JarContent)content, name, this) is not called. However, for a DEX-converted JAR file, because it does not matter whether bytes is null or not, in other words, because it does not matter whether xxx.class exists in the JAR file or not (because classes.dex is contained alternatively), the check of the if statement is unnecessary. As a matter of fact, if the code is changed so that the content in the if block can be executed even when bytes is null, an OSGi bundle installed on Felix on Android can be happily started.


Additionally, if getDexFileClass() returns null, the following defineClass() is called with bytes == null. To avoid this, the condition of the if block enclosing the defineClass() call has to be changed from if (clazz == null) to if (clazz == null && bytes != null). Otherwise, SIGSEGV error occurs on Android emulator. (I don't know why NullPointerException is thrown instead.)