javac++mavenjava-native-interfacemaven-nar-plugin

Native code for JUnit tests not compiled with `nar-maven-plugin`


I have a JNI project configured with Maven using the nar-maven-plugin. Both the Java and the C++ code reside in the project. The main code apparently compiles properly (both C++ and Java). The problem is with the test code (JUnit).

The test code defined one Java class that itself has a native method. The corresponding native code resides in the directory

<project root>
+- src
   +- test
      +- c++

There is no evidence from the build messages that this native test code is ever compiled and the corresponding native method does not appear at all when I run nm from the command line on the DLL created by the build process. In addition, I intentionally put a syntax error into the test code and recompiled to see if I would get a compile-time error. There is no error, consistent with my belief that the code is never compiled.

Correspondingly, I get an UnsatisfiedLinkError when the test runs during mvn install. Note that I can tell from the point at which the test failed that the native methods for the main (non-test) code were properly loaded and linked. Hence I conclude that there is some problem related to the building and linking of native test code specifically.

I'm currently on Windows 10 using the Eclipse IDE and MinGW compilers for native code.

The relevant sections of my POM are below (slightly updated from my answer on Avoiding machine-dependent POM with MinGW compiler and nar-maven-plugin related to an early configuration problem):

<profiles>
    <profile>
        <id>Windows-MinGW</id>
        <activation>
            <os>
                <family>Windows</family>
            </os>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <groupId>com.github.maven-nar</groupId>
                    <artifactId>nar-maven-plugin</artifactId>
                    <version>3.5.1</version>
                    <extensions>true</extensions>
                    <configuration>
                        <cpp>
                             <options>
                                  <option>-std=c++1y</option>
                             </options>
                        </cpp>

                        <linker>
                            <name>g++</name>
                            <options>
                                <option>-Wl,--kill-at</option>
                            </options>
                        </linker>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

<build>
    <defaultGoal>integration-test</defaultGoal>

    <plugins>
        <plugin>
            <groupId>com.github.maven-nar</groupId>
            <artifactId>nar-maven-plugin</artifactId>
            <version>3.5.1</version>
            <extensions>true</extensions>
            <configuration>
                <cpp>
                    <defines>
                        <define>EXPORT_DLL</define>
                    </defines>
                </cpp>
                <libraries>
                    <library>
                        <type>jni</type>
                        <narSystemPackage>com.mycompany.sandbox</narSystemPackage>
                    </library>
                </libraries>
            </configuration>
        </plugin>
    </plugins>

</build>

Is there a known way to handle this problem? (Maybe additional configuration tags?)


Solution

  • I got this to work, but it wasn't pretty. I'll give the POM parts at the end, but, in outline, these are the steps:

    Expanding on the last bullet in more detail: Even once you get the test code to compile, you won't be able to load it from Java because the test directories are not on the java.library.path when the tests are run! The best solution that I could find was to move the test library temporarily into a directory that is on the path. This seemed easier than changing the path due to the configuration options easily available. Provided that you get the library on the path, then you can use System.loadLibrary as usual during the JUnit test execution.

    Now here are the expanded POM segments that accomplish the above. This is based on what I had in the question, but with the new pieces requires to accomplish the bullets at the beginning of the answer. Note that the JNI code backing the test is in a file TestWrapper.cpp in /src/test/c++. (I know that's not standard naming convention for a JNI source file.)

    NOTE: At this point I have only worked out the linker flags for testing on my Windows 10 machine, which is representing in the profiles section. Likewise, the copy / delete has the .dll extension explicitly, which would need to be adjusted. (Further note, however, that even though you get a .dll file, it will have a .exe extension when the plugin makes it!) My POM is no doubt broken for other machines / architectures at this point, but there's a clear path forward to making them work from here, so it seems worth posting the answer as is.

    <profiles>
        <profile>
            <id>Windows-MinGW</id>
            <activation>
                <os>
                    <family>Windows</family>
                </os>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>com.github.maven-nar</groupId>
                        <artifactId>nar-maven-plugin</artifactId>
                        <version>3.5.1</version>
                        <extensions>true</extensions>
                        <configuration>
                            <cpp>
                                <options>
                                    <option>-std=c++1y</option>
                                </options>
                            </cpp>
                            <linker>
                                <name>g++</name>
                                <options>
                                    <option>-Wl,--kill-at</option>
                                </options>
                                <testOptions>
                                    <!-- Put the -shared flag onto the linker - That will force a DLL instead of an EXE -->
                                    <testOption>-shared</testOption>
                                    <!-- We cannot easily link to the *library* that was created for the main project but we can get the compiled object files with the following option -->
                                    <testOption>${project.build.directory}/nar/obj/${nar.aol}/*.o</testOption>
                                </testOptions>
                            </linker>
                        </configuration>
                    </plugin>                   
                </plugins>
            </build>
        </profile>
    </profiles>
    
    <build>
        <defaultGoal>integration-test</defaultGoal>
    
        <plugins>
            <plugin>
                <groupId>com.github.maven-nar</groupId>
                <artifactId>nar-maven-plugin</artifactId>
                <version>3.5.1</version>
                <extensions>true</extensions>
                <configuration>
                    <cpp>
                        <defines>
                            <define>EXPORT_DLL</define>
                        </defines>
                    </cpp>
                    <libraries>
                        <library>
                            <type>jni</type>
                            <narSystemPackage>com.mycompany.mypackage</narSystemPackage>
                        </library>
                    </libraries>
                    <tests>
                        <test>
                            <name>TestWrapper</name>
                            <run>false</run>
                        </test>
                    </tests>
                </configuration>
            </plugin>
    
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.7</version>
                <executions>
                    <execution>
                        <id>copy-test-lib-to-path</id>
                        <phase>pre-integration-test</phase>
                        <configuration>
                            <target>
                                <copy file="${project.build.directory}/test-nar/bin/${nar.aol}/TestWrapper.exe" tofile="${project.build.directory}/nar/${project.artifactId}-${project.version}-${nar.aol}-jni/lib/${nar.aol}/jni/TestWrapper.dll"/>
                            </target>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>delete-test-lib-from-deployment</id>
                        <phase>post-integration-test</phase>
                        <configuration>
                            <target>
                                <delete file="${project.build.directory}/nar/${project.artifactId}-${project.version}-${nar.aol}-jni/lib/${nar.aol}/jni/TestWrapper.dll"/>
                            </target>
                        </configuration>
                        <goals>
                            <goal>run</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
    
        </plugins>