home about map mail
 backgrounders 
 code & components 
 other resources 

OPC and Java JNI Tutorial

This tutorial, originally published by Naknan, will show the high points of how to connect Java(tm) to an OPC Alarm and Event Server using JNI. The code is available for download and may be used for educational purposes. This write up assumes a familarity with Java(tm) and OPC. It attempts to help the user understand the code and why it is written the way it is.

Writing the Java Code (Native Methods)

Generating the JNI Header file from your class file

Tips when writing the C++ side (Native Implementation)

Getting the events into Java

Pulling it together

Download the source

Requirements

Disclaimer

Java Code

The first thing we will take a look at is how to define a native method in Java(tm) and how to use that method in connecting to our OPC Alarm & Event Server.

--- Snip ---
public class Opc
  implements Runnable
{
  public native boolean connect_p(Opc ae_callback);
  public native boolean init_p(String host_name, String server_name);

  public static void main(String args[])
  {
    Opc opc_connection = new Opc(args[0], args[1]);
    (opc_connection.getThread()).start();
  } // main
--- Snip ---

First we notice the use of the keyword native on lines 4 and 5. This tells the Java(tm) compiler that the implementation of this methods will be provided via an external native library (ie code not written in Java(tm))

Next we look at how to call a method declared to be native. It turns out that calling a native method is no different than calling a normal Java(tm) method, as we can see in line 6 below.

--- Snip ---
  private boolean init()
  {
    if (inited)
      return true;

    return (inited = init_p(host_name, server_name));
  } // init
--- Snip ---

The key is that before calling any methods declared to be native we must first load the library that contains the method body. We see on line 3 below that Java(tm) provides a simple way to load external librarys into the VM. Also notice that we don't have to put an extension on the library name. Java(tm) automatically adds the correct extension for the operating system (ie .dll for windows, .so for unix, etc).

--- Snip ---
  try
  {
    System.loadLibrary("OPC");
  }
  catch(Exception e)
  {
    System.err.println("Unable to load library - " +
      System.mapLibraryName("OPC"));
  }
--- Snip ---

Java Header

Once you have your Java(tm) code written and compiled, it's time to generate the header file that will allow Java(tm) to call the appropriate method(s) in the native code. To do this, we run the javah utility passing our class as an argument to it.

javah -jni com.naknan.example.opc.Opc

Notice that we don't pass the .java source file, we pass in the .class file without the .class extension. The javah utility then generates a file based on the name of the class passed to it. In our case, it generates a file named: com_naknan_example_opc_Opc.h. The contents of this file look simular to this.

   /* DO NOT EDIT THIS FILE - it is machine generated */
   #include 
   /* Header for class com_naknan_example_opc_Opc */

   #ifndef _Included_com_naknan_example_opc_Opc
   #define _Included_com_naknan_example_opc_Opc
   #ifdef __cplusplus
   extern "C" {
   #endif
   /*
   * Class:     com_naknan_example_opc_Opc
   * Method:    connect_p
   * Signature: (Lcom/naknan/example/opc/Opc;)Z
   */
   JNIEXPORT jboolean JNICALL Java_com_naknan_example_opc_Opc_connect_1p
     (JNIEnv *, jobject, jobject);

   /*
   * Class:     com_naknan_example_opc_Opc
   * Method:    init_p
   * Signature: (Ljava/lang/String;Ljava/lang/String;)Z
   */
   JNIEXPORT jboolean JNICALL Java_com_naknan_example_opc_Opc_init_1p
     (JNIEnv *, jobject, jstring, jstring);

   #ifdef __cplusplus
   }
   #endif
   #endif

C++ Tips

The Visual C++ project that comes as part of the download contains a project settings that may need to be changed in order for you to build it successfully.

Under Project->Settings on the C/C++(tab) in the Prepocessor(Catagory)
Set the appropriate path for your JDK include. It assumes c:\jdk1.3\

Also modified were the Precompiled Headers(Catagory), to "Not using precompiled headers" and under the General(Catagory) we added _WIN32_DCOM to the Preprocessor definitions inorder to get the COM/DCOM environment.

COM/DCOM Initialization

In initialization of the COM/DCOM environment on line 2 below, we use the COINIT_MULTITHREADED options which will allow multiple threads (JavaVM and Alarm & Event Server) to share data.

--- Snip ---
  // Initialize the COM/DCOM environment for multi-threaded access
  result = CoInitializeEx(NULL, COINIT_MULTITHREADED);

  result = CoGetMalloc(MEMCTX_TASK, &pIMalloc);

  if (FAILED(result))
  {
    CoUninitialize();
    return false;
  }
--- Snip ---

Connecting to an OPC Alarm & Event Server

--- Snip ---
  // Retrieve the Class ID based on the Program ID
  CLSIDFromProgID(WSTRFromSBCS(server_name, pIMalloc), &clsid);

  // Get the Class Object
  CoGetClassObject(clsid, CLSCTX_LOCAL_SERVER, NULL, 
    IID_IClassFactory, (void **)&factory);

  // Create an Instance (attach if already created)
  factory->CreateInstance(NULL, IID_IUnknown,(void **)&objserver);

  // Query for the EventServer Interface
  objserver->QueryInterface(IID_IOPCEventServer, (void**)&ae_server);

  // Get EventSubscriptionMgt Interface
  ae_server->CreateEventSubscription(true, 0, 0, handle,
    IID_IOPCEventSubscriptionMgt, &objsubscription,
    &revisedBufferTime, &revisedMaxSize);

  // Query for the Connection Point Interface
  ((IOPCEventSubscriptionMgt *)objsubscription)->QueryInterface(&cpc);

  // Get the Connection Point
  cpc->FindConnectionPoint(IID_IOPCEventSink, &cp);

  // Create an object for callback events from the A&E Server
  iopces = new JNISink();

  // Register Sink Object for Event Notification
  result = cp->Advise(iopces, &cookie);
--- Snip ---

As events are generated in the Alarm & Event Server, the OnEvent method of the JNISink object will be called. Once the Event is received it can be handled in any way, even passed into Java(tm).

Getting the events into Java

Below is the implementation of the OnEvent method of our JNISink object. This method will be called from the OPC Alarm & Event Server when a new event has been generated. This method then gathers all the information from the ONEVENTSTRUCT and converts it the the appropriate Java(tm) types and after connecting to the Java(tm) VM and looking up the callback method, forwards the OPC Alarm/Event to the Java(tm) side for further processing.

--- Snip ---
  HRESULT STDMETHODCALLTYPE JNISink::OnEvent( 
    /* [in] */ OPCHANDLE hClientSubscription,
    /* [in] */ BOOL bRefresh,
    /* [in] */ BOOL bLastRefresh,
    /* [in] */ DWORD dwCount,
    /* [size_is][in] */ ONEVENTSTRUCT __RPC_FAR *pEvents)
  {
    JNIEnv *jni_env = NULL;

    // Play nice and connect
    jni_vm->AttachCurrentThread((void **)&jni_env, NULL);

    // Just a dummy value so that we don't send null to the java side
    const char *UnKnown = "Unknown";

    // Only do this once...
    if (lookupMethod)
    {
      jclass clazz = jni_env->GetObjectClass(jni_callback);

      // Use "javap -s" to generate signature
      mid = jni_env->GetMethodID(clazz, "addEvent", 
        "(Ljava/lang/String;IIIIIIIILjava/lang/String;" + 
        "IIILjava/lang/String;Ljava/lang/String;IZIIIII" +
        "IIIILjava/lang/String;II)V");
   
      lookupMethod = false;
    }

    // Initialize some jstrings...
    jstring source = jni_env->NewStringUTF(UnKnown);
    jstring message = jni_env->NewStringUTF(UnKnown);
    jstring condition = jni_env->NewStringUTF(UnKnown);
    jstring subcondition = jni_env->NewStringUTF(UnKnown);
    jstring actor = jni_env->NewStringUTF(UnKnown);

    int len = 0;

    if ((len = wcslen(pEvents->szSource)) > 0)
      source = jni_env->NewString(pEvents->szSource, len + 1);

    if ((len = wcslen(pEvents->szMessage)) > 0)
      message = jni_env->NewString(pEvents->szMessage, len + 1);

    if ((len = wcslen(pEvents->szConditionName)) > 0)
      condition = jni_env->NewString(pEvents->szConditionName, len + 1);

    if ((len = wcslen(pEvents->szSubconditionName)) > 0)
      subcondition = jni_env->NewString(pEvents->szSubconditionName, len + 1);

    if ((len = wcslen(pEvents->szActorID)) > 0)
      actor = jni_env->NewString(pEvents->szActorID, len + 1);

    SYSTEMTIME time;
    FileTimeToSystemTime(&pEvents->ftTime, &time);

    SYSTEMTIME activetime;
    FileTimeToSystemTime(&pEvents->ftActiveTime, &activetime);

    // Call the "addEvent" method which is void with the correct data
    jni_env->CallVoidMethod(jni_callback, mid, source, time.wYear, time.wMonth,
      time.wDay, time.wDayOfWeek, time.wHour, time.wMinute, time.wSecond,
      time.wMilliseconds, message, pEvents->dwEventType,
      pEvents->dwEventCategory, pEvents->dwSeverity, condition,
      subcondition,
      pEvents->wQuality, pEvents->bAckRequired, activetime.wYear,
      activetime.wMonth, activetime.wDay, activetime.wDayOfWeek,
      activetime.wHour, activetime.wMinute, activetime.wSecond,
      activetime.wMilliseconds, pEvents->dwCookie, actor,
      pEvents->wChangeMask, pEvents->wNewState);

    // Play nice and disconnect
    jni_vm->DetachCurrentThread();

    return S_OK;
  } // OnEvent
--- Snip ---

Pulling it together

Once you have the VC++ project built, copy the .dll that was created into the directory containing your .class file. The System.loadLibrary call will look for it here as well as a few other places, but I have found that it is simpler to place it in the same directory as the .class file.

Sample Windows(tm) .bat file used for running the system.

c:
cd \
cd cvs\com\naknan\example\opc
java -cp c:\cvs com.naknan.example.opc.Opc FactorySoft.Example.Alarm.1 asylum

Download

Download the source code for this tutorial.

Requirements

The VC++ project was created and tested under VC++ 6.0. The Java(tm) code was compiled and run under the JDK 1.3 VM from Sun Microsystems.

You will also need to have an OPC Alarm & Event Server installed. I use the Alarm & Event Server Demo server from FactorySoft, but any OPC Alarm & Event server will work (demo or not).

Disclaimer

Information on OPC can be found at the OPC Foundation.

This code is provided for educational purposes only. It is provided as-is and without support or warranty of any sort. You are free to study this code and to use the techniques in your own implementation. Parts of this example are left up to the user and it is assumed that the user will be knowledgeable in VC++ and OPC Alarm & Event terminology and data types.

This code has not been reviewed, tested or approved by the OPC Foundation.

You may also freely incorporate parts of this code into an implementation as long as the following appears in each source file:

"(c) Copyright 2001-2007 Naknan Inc and Your_Company_Name ALL RIGHTS RESERVED"
"Based on Sample code provided by Naknan, Inc."

home about map mail