Monday, July 21, 2014

Creating Java libraries from DLLs for Concept2 ergometer

I have a Concept2 rower with a PM4, which has a SDK, which can be downloaded from the Concept2 website. The SDK provides some DLLs, which can be used to access the rower's PM4 computer. This blog describes how I generated some Java libraries using gluegen and Microsoft Visual Studio 2010 to access to DLLs with Java.

Because the SDK provides the C header files for its DLLs, I was able to give these as input to the gluegen program and automatically generate the Java and JNI code necessary to call Concept2 libraries. Below is an excerpt from the generated JNI code for the tkcmdsetCSAFE_command function. 


/*   Java->C glue code:
 *   Java package: org.wmmnpr.c2.csafe.PM3CsafeCP
 *    Java method: short tkcmdsetCSAFE_command(short unit_address, short cmd_data_size, java.nio.LongBuffer cmd_data, java.nio.ShortBuffer rsp_data_size, java.nio.LongBuffer rsp_data)
 *     C function: ERRCODE_T tkcmdsetCSAFE_command(UINT16_T unit_address, UINT16_T cmd_data_size, UINT32_T *  cmd_data, UINT16_T *  rsp_data_size, UINT32_T *  rsp_data);
 */
JNIEXPORT jshort JNICALL 

Java_org_wmmnpr_c2_csafe_PM3CsafeCP_tkcmdsetCSAFE_1command1__SSLjava_lang_Object_2IZLjava_lang_Object_2IZLjava_lang_Object_2IZ(JNIEnv *env, jclass _unused, jshort unit_address, jshort cmd_data_size, jobject cmd_data, jint cmd_data_byte_offset, jboolean cmd_data_is_nio, jobject rsp_data_size, jint rsp_data_size_byte_offset, jboolean rsp_data_size_is_nio, jobject rsp_data, jint rsp_data_byte_offset, jboolean rsp_data_is_nio) {
  UINT32_T * _cmd_data_ptr = NULL;
  UINT16_T * _rsp_data_size_ptr = NULL;
  UINT32_T * _rsp_data_ptr = NULL;

  ERRCODE_T _res;

  if ( NULL != cmd_data ) {
    _cmd_data_ptr = (UINT32_T *) ( JNI_TRUE == cmd_data_is_nio ?  (*env)->GetDirectBufferAddress(env, cmd_data) :  (*env)->GetPrimitiveArrayCritical(env, cmd_data, NULL) );  }
  if ( NULL != rsp_data_size ) {
    _rsp_data_size_ptr = (UINT16_T *) ( JNI_TRUE == rsp_data_size_is_nio ?  (*env)->GetDirectBufferAddress(env, rsp_data_size) :  (*env)->GetPrimitiveArrayCritical(env, rsp_data_size, NULL) );  }
  if ( NULL != rsp_data ) {
    _rsp_data_ptr = (UINT32_T *) ( JNI_TRUE == rsp_data_is_nio ?  (*env)->GetDirectBufferAddress(env, rsp_data) :  (*env)->GetPrimitiveArrayCritical(env, rsp_data, NULL) );  }
  _res = tkcmdsetCSAFE_command((UINT16_T) unit_address, (UINT16_T) cmd_data_size, (UINT32_T *) (((char *) _cmd_data_ptr) + cmd_data_byte_offset), (UINT16_T *) (((char *) _rsp_data_size_ptr) + rsp_data_size_byte_offset), (UINT32_T *) (((char *) _rsp_data_ptr) + rsp_data_byte_offset));

  if ( JNI_FALSE == cmd_data_is_nio && NULL != cmd_data ) {
    (*env)->ReleasePrimitiveArrayCritical(env, cmd_data, _cmd_data_ptr, 0);  }
  if ( JNI_FALSE == rsp_data_size_is_nio && NULL != rsp_data_size ) {
    (*env)->ReleasePrimitiveArrayCritical(env, rsp_data_size, _rsp_data_size_ptr, 0);  }
  if ( JNI_FALSE == rsp_data_is_nio && NULL != rsp_data ) {
    (*env)->ReleasePrimitiveArrayCritical(env, rsp_data, _rsp_data_ptr, 0);  }
  return _res;

}

The first difficulty experienced was that the DLLS, even though they were written in C, were compiled as C++ code, which meant that when the JNI generated code was compiled as C files, it could not be linked against the C++ static libraries (PM3CsafeCP.lib, PM3DDICP.lib and PM3USBCP.lib); the external references, namely the functions I wanted to call in the DLLs, were mangled according to C++ rules rather than C ones, which my C object files expected. The link error was as follows:

error LNK2019: unresolved external symbol "__imp__tkcmdsetCSAFE_async_command" ...

The only option was to compile the gluegen generated JNI C files are C++ files; however, that wouldn't work because the "__cplusplus" macros in the jni.h header file lead to compile errors; namely:

error C2819: type 'JNIEnv_' does not have an overloaded member 'operator ->'

I fixed this by commenting out the "__cplusplus" macros and their corresponding code in a local copy of the jni.h file so that the struct JNIEnv_ definition would stay the same and not change for C++ compilation modules. The changes allowed me to compile my C code with the C++ compiler without any errors and thereafter link against the PM4 C++ DLLs; which were really C ones. 

The next error occurred during runtime. Loading the DLLs made from the gluegen code with "System.loadLibrary
" was no problem given that the DLLs created with the code from gluegen and the ones from the SDK (PM3CsafeCP.dll, PM3DDICP.dll and PM3USBCP.dll) were located in the in the JVM's search path. My solution was to set the VM argument "java.library.path" to the proper location of the DLLs, which I had placed in one directory.

The first indication that something was wrong with my DLLs occurred when trying to access one of the exported functions; in which case, the following error occurred:

Exception in thread "main" java.lang.UnsatisfiedLinkError: org.wmmnpr.c2.ddi.PM3DDICP.tkcmdsetDDI_init()S
at org.wmmnpr.c2.ddi.PM3DDICP.tkcmdsetDDI_init(Native Method)

Again a problem with the name mangling. Because I had compiled my generated gluegen C source files as C++ source files into DLLs, the Java runtime could find them under the signatures generated by gluegen. The solution was the to make sure the the exported functions in the C modules were mangled using C mangling rules. This I did by enclosing my source code in 

extern "C" {
/* code from gluegen */
}

Enclosing the header file declarations would have been better but gluegen didn't produce any header files; only source ones.


With those three errors solved I was able to write a Java program to access my PM4.