How to integrate a Microsoft Visual Basic or C# .NET Component into SageTV using the SageTV Studio

 

Introduction

 

The files for the examples here at available at: http://download.sage.tv/IntegrateVBDotNetWithSageTVStudio-0.3.zip

 

One of the nice features of the SageTV studio is its ability to call arbitrary Java code. This allows SageTV to integrate with a large number of different components. One of the more useful techniques for integration is JNI. JNI is the Java to Native Interface supported by Sun Microsystems. This interface allows Java method calls to be directly translated into corresponding C/C++ method calls. From there, anything that C/C++ can integrate with can be used (which is pretty much everything).

 

In this session, we'll show how to use JNI to integrate with a Visual Basic or C# .NET component which is then tied into SageTV using the Studio.

 

This example uses the SystemMonitorPlugin contributed by deria. Many thanks for his contribution. J

 

Part 1: The Visual Basic or C# .NET Component

 

(We'll be using a Visual Basic component for the example here, but the interfaces exposed by VB and C# .NET components are the same)

This component is written by the Visual Basic developer. The developer will then decide what functionality of the component that they wish to expose to the SageTV environment. Here's a quick explanation of the SytemMonitorPlugin VB code we'll be using in this example:

 

A host initializes the plugin by calling Startup in the InLibraryIntermediary class.  The InLibraryIntermediary class instantiates the Plugin class by calling Initialize and passing the addresses of two callback functions that will be used to allow the plugin to communicate with the host.  The initialize function of the Plugin class actually creates a secondary thread that does all the real work (and forwards the callback functions to it so that it can communicate with the host too).  The secondary thread can receive messages passed into it via the RegisterEvent function of the Plugin class.  In addition, the public functions of the secondary thread class are accessible from the Plugin class.  Its a little complicated, but the end result is that once the plugin is initialized it operates in its own thread and can both send and receive information from the host without interfering with the host.

 

So to use this plugin we need to create an instance of the InLibraryIntermediary class. Then we'll need to call Startup() on that object to initialize it; and also call Shutdown() when we're done wiith it. The other two methods it exposes which are of interest to us are GetDiskUsageText() and GetDiskUsageGraph(). These return a String and Bitmap respectively.  These calls can be used to dispay textual information about the disks on the system as well as a graphical representation.

 

Part 2: The Java Class

 

Next we'll write the Java class which will be used to declare the native methods that we can then implement in JNI.  This Java class is also what will be called from the SageTV Studio directly. We'll call the class SystemMonitorPlugin and put it in the default package. The Java code is actually pretty small, so we'll put it all here and explain it. This should go in a file named SystemMonitorPlugin.java.

 

public class SystemMonitorPlugin

{

            static

            {

                        System.loadLibrary("SystemMonitorJNI");

            }

 

            /** Creates a new instance of SystemMonitorPlugin */

            public SystemMonitorPlugin()

            {

            }

           

            public native void Startup();

            public native void Shutdown();

            public native String GetDiskUsageText();

            public native java.awt.Image GetDiskspaceImage();

           

            private long nativePtr;

}

 

We start off with our class definition for SystemMonitorPlugin.  After that we add a static initializer which is used to load the DLL for the JNI implementation. This means the DLL we'll compile later will need to be named SystemMonitorJNI.dll and it will need to be accessible via the Java native library path when SageTV is invoked.  We also have an empty constructor which will be used to create the object.  We then have declarations for the 4 native methods that we want to access the VB object. By declaring them native this tells Java to use JNI to resolve these methods.  Lastly, we have a field called nativePtr. This is used to hold a C pointer to the object that will be created in the DLL. This allows Java to hold onto what it has created in the native layer.  This field is get/set from the native layer.

 

To compile this class, we need to have the JDK installed. This is freely available from Sun Microsystems at: http://java.sun.com/j2se/1.5.0/download.jsp Be sure to download the JDK and not the JRE. Then we can just use the command line and cd into the directory where the .java file is. Then type:

javac SystemMonitorPlugin.java

 

Then you should have a SystemMonitorPlugin.class file created in that same folder. That is the compiled Java byte code.

 

Part 3: The JNI DLL

 

The first thing to do to create a JNI DLL is to generate the headers using Java. This is done using the javah tool which is installed with the JDK. Use the command line and cd into the directory where the compiled Java class file is. Then run this:

javah -classpath . SystemMonitorPlugin

 

That will create a file called SystemMonitorPlugin.h in the current directory. That is the header file for the JNI DLL implementation.

 

There's 2 ways you can create the DLL. The first is using Visual Studio .NET from Microsoft (which is not free). The second way, we explain how to do it using freely available tools from Microsoft.

 

Using Visual Studio .NET to compile the DLL

 

Now we use Microsoft Visual Studio .NET to create a new project that we'll use to build the DLL. Open up MS Visual Studio .NET, then do the following:

  1. Go to File->New->Project
  2. From the left column choose 'Visual C++ Project'->Win32, and from the right column choose 'Win32 Project'. For the name, enter SystemMonitorJNI. Be sure the 'Close Solution' button is selected if available. Then click OK.
  3. In the Application Wizard dialog, select Application Settings from the left side. Then on the right select DLL for Application Type. Click Finish.
  4. Using Windows Explorer, copy the SystemMonitorPlugin.h file you generated in the first step of Part 3 into the SystemMonitorJNI\SystemMonitorJNI directory that was just created for the Visual Studio project.
  5. Going back to Visual Studio, right-click on Header Files and select Add->Add Existing Item. Then navigate to the SystemMonitorPlugin.h file you just copied and select that file.
  6. Copy the files ManagedToJava.cpp and ManagedToJava.h into that same directory. Add the .cpp file to the 'Source Files' group and add the .h file to the 'Header Files' group. These files are contained in the download zip from http://download.sage.tv/IntegrateVBDotNetWithSageTVStudio-0.3.zip
  7. Open the file SystemMonitorJNI.cpp in the editor and add the following code to it:

 

// Include the header file generated by javah. These are the functions

// we need to define in this file.

#include "SystemMonitorPlugin.h"

 

// Include MS headers for using Managed .NET code

#using <mscorlib.dll>

#include <vcclr.h>

 

// Include required for Image operations used in this file

#using <System.Drawing.dll>

 

#include "ManagedToJava.h"

 

// These struct definitions resolve a TypeLoadException in the managed code

// that would otherwise occur at runtime.

struct _jmethodID {};

struct _jfieldID {};

 

// Include the compiled managed DLL file we'll be interfacing to

#using "BasicSystemMonitor.dll"

 

// This struct is used to wrap the managed pointer. We can then cast the

// struct pointer to a jlong and store it inside our Java object.

typedef struct

{

         gcroot<BasicSystemMonitor::InLibraryIntermediary*> pPluggy;

} NativeObjectPointer;

 

/*

 * Class:     SystemMonitorPlugin

 * Method:    Startup

 * Signature: ()V

 */

JNIEXPORT void JNICALL Java_SystemMonitorPlugin_Startup

  (JNIEnv *env, jobject jo)

{

   // Get access to the Java object field that holds the NativeObjectPointer

   static jclass jc = env->GetObjectClass(jo);

   static jfieldID ptrFid = env->GetFieldID(jc, "nativePtr", "J");

 

   // Dynamically allocate a new struct on the heap that will hold the

// managed object pointer.This is the pointer that we'll store in our

// Java object.

    NativeObjectPointer *myPtr = new NativeObjectPointer;

 

   // Create the VB .NET object and put the pointer to it inside our struct

   myPtr->pPluggy = new BasicSystemMonitor::InLibraryIntermediary();

 

   // Call the Startup() method on the VB .NET component

   myPtr->pPluggy->Startup();

 

   // Set the field in our Java object that holds the NativeObjectPointer.

// Then we can reuse this on later method calls.

   env->SetLongField(jo, ptrFid, (jlong)myPtr);

}

 

/*

 * Class:     SystemMonitorPlugin

 * Method:    Shutdown

 * Signature: ()V

 */

JNIEXPORT void JNICALL Java_SystemMonitorPlugin_Shutdown

  (JNIEnv *env, jobject jo)

{

   // Get access to the Java object field that holds the NativeObjectPointer

   static jfieldID ptrFid =

env->GetFieldID(env->GetObjectClass(jo), "nativePtr", "J");

 

   // Cast the field's value to the struct pointer so we can access its contents.

   NativeObjectPointer* myPtr =

(NativeObjectPointer*) env->GetLongField(jo, ptrFid);

   if (myPtr)

   {

         // Call Shutdown() on the VB .NET plugin component

         myPtr->pPluggy->Shutdown();

 

         // Clear the value for the managed pointer which should allow GC

// of it to occur

         myPtr->pPluggy = NULL;

 

         // Free the memory we allocated on the heap for our NativeObjectPointer

         delete myPtr;

 

         // Clear the field in the Java object so we don't try to access

// an invalid pointer accidentally later

         env->SetLongField(jo, ptrFid, 0);

   }

}

 

/*

 * Class:     SystemMonitorPlugin

 * Method:    GetDiskUsageText

 * Signature: ()Ljava/lang/String;

 */

JNIEXPORT jstring JNICALL Java_SystemMonitorPlugin_GetDiskUsageText

  (JNIEnv *env, jobject jo)

{

   // Get access to the Java object field that holds the NativeObjectPointer

   static jfieldID ptrFid =

env->GetFieldID(env->GetObjectClass(jo), "nativePtr", "J");

 

   // Cast the field's value to the struct pointer so we can access its contents.

   NativeObjectPointer* myPtr =

(NativeObjectPointer*) env->GetLongField(jo, ptrFid);

   if (myPtr)

   {

         // Call the GetDiskUsageText() method on the VB .NET component to

// get the managed String object that holds the value.

         return MgdStringToJString(env, myPtr->pPluggy->GetDiskUsageText());

   }

   return NULL;

}

 

/*

 * Class:     SystemMonitorPlugin

 * Method:    GetDiskspaceImage

 * Signature: ()Ljava/awt/Image;

 */

JNIEXPORT jobject JNICALL Java_SystemMonitorPlugin_GetDiskspaceImage

  (JNIEnv *env, jobject jo)

{

   // Get access to the Java object field that holds the NativeObjectPointer

   static jfieldID ptrFid =

env->GetFieldID(env->GetObjectClass(jo), "nativePtr", "J");

 

   // Cast the field's value to the struct pointer so we can access its contents.

   NativeObjectPointer* myPtr =

(NativeObjectPointer*) env->GetLongField(jo, ptrFid);

   if (myPtr)

   {

         // Call the GetDiskUsageGraph() method on the VB .NET component to

// get the managed Bitmap object that has the image contents we

// want to return

         return MgdImageToBufferedImage(env, myPtr->pPluggy->GetDiskUsageGraph());

   }

   return NULL;

}

 

Within the comments for the above code are descriptions for what it all does.

 

That's it for the native code. Now we need to configure the project settings in order for it to compile.

  1. Right-click on the project in the Solution Explorer and select Properties
  2. From the Configurations drop down, select All Configurations
  3. Select Configuration Properties->C/C++->General from the left side
  4. For 'Resolve #using References' enter: $(outdir)
  5. For 'Additional Include Directories' enter the path to the include subdirectory of your JDK (Java Development Kit) installation. Also add the path to the include\win32 directory in that same install. For example, mine is: C:\jdk\include;C:\jdk\include\win32
  6. Select Configuration Properties->C/C++->Precompiled Headers from the left side
  7. For 'Create/Use Precompiled Headers' select Not Using Precompiled Headers
  8. Select Configuration Properties->Build Events->Pre-Build Events from the left side
  9. For 'Command Line' enter the following: copy "..\..\BasicSystemMonitor_Plugin\bin\BasicSystemMonitor.dll"  $(ConfigurationName)
  10. Adjust the 'Command Line' entry you just made to match the path for where you extracted the example to so it can find the BasicSystemMonitor.dll file correctly.
  11. Click on the Apply button, the properties dialog will remain open.
  12. In the 'Solution Explorer', click on the SystemMonitorJNI.cpp file. This will change the contents of the Properties dialog.
  13. Select Configuration Properties->C/C++->General from the left side
  14. For 'Compile as Managed' select Assembly Support (/clr)
  15. Click on Apply
  16. In the 'Solution Explorer', click on the ManagedToJava.cpp file. This will change the contents of the Properties dialog.
  17. Select Configuration Properties->C/C++->General from the left side
  18. For 'Compile as Managed' select Assembly Support (/clr)
  19. Click on Apply
  20. From the Configurations drop down, select Active(Debug)
  21. Select Configuration Properties->C/C++->Code Generation from the left side
  22. For 'Basic Runtime Checks' select Default
  23. For 'Enable Minimal Rebuild' select No
  24. Select Configuration Properties->C/C++->General from the left side
  25. For 'Debug Information Format' select Program Database
  26. Click on OK

 

The project should now build. So select Build->Build Solution and check for any errors.

 

Using free tools from Microsoft to build the DLL

 

There's 2 things you need to download and install from Microsoft to build the DLL without Visual Studio.

  1. Visual C++ Toolkit 2003 - http://www.microsoft.com/downloads/details.aspx?FamilyId=272BE09D-40BB-49FD-9CB0-4BFA122FA91B&displaylang=en
  2. Windows Platform SDK (the download site may indicate it's Windows Server, but it's still the same one)- http://www.microsoft.com/downloads/details.aspx?FamilyId=A55B6B43-E24F-4EA3-A93E-40C0EC4F68E5&displaylang=en

 

Then you can just make a directory somewhere called SystemMonitorJNI and put the following files from the example into it:

  1. BasicSystemMonitor.dll
  2. ManagedToJava.cpp
  3. ManagedToJava.h
  4. stdafx.h
  5. SystemMonitorJNI.cpp
  6. SystemMonitorPlugin.h

 

Note: You may need to run the program C:\Program Files\Microsoft Visual C++ Toolkit 2003\vcvars32.bat to setup your environment variables correctly.

 

Then create a new file in that directory called build.bat and then open build.bat with Notepad to edit it. Copy and paste the following as the contents of the file (the file contains 2 long lines, be sure to remove any wrapping that occurs from copy/paste).

 

 

rem Set this path to the path where you installed the Microsoft Platform SDK.

set PSDKPATH=C:\Program Files\Microsoft Platform SDK

rem Set this path to the path where you installe the Java SDK.

set JAVAPATH=C:\Program Files\Java\jdk1.5.0_05

@echo off

Echo Compiling...

 

cl.exe /I "%PSDKPATH%\Include" /O2 /I "%JAVAPATH%\include" /I "%JAVAPATH%\include\win32" /AI "." /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_USRDLL" /D "SYSTEMMONITORJNI_EXPORTS" /D "_WINDLL" /D "_MBCS" /EHsc /MT /Fo"./" /W3 /c /clr /TP ".\SystemMonitorJNI.cpp" ".\ManagedToJava.cpp"

 

echo Linking...

 

link.exe /LIBPATH:"%PSDKPATH%\Lib" /OUT:"./SystemMonitorJNI.dll"
/INCREMENTAL:NO /NOLOGO /DLL /SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /MACHINE:X86 
kernel32.lib ".\ManagedToJava.obj" ".\SystemMonitorJNI.obj"

 

echo Done.

pause

 

 

Then double click on the file build.bat and check it for any errors. If it worked correctly you should have a SystemMonitorJNI.dll file in the folder you just created. Ignore any warnings in the build process, they are expected.

 

Part 4: Putting it all together in the SageTV Studio

 

Now we're onto the final step where we actual see the result of our work. Now you'll need to copy the compiled code you just generated into the SageTV program folder (the same place that the SageTV.exe program is at). The files that you need to copy there are:

  1. BasicSystemMonitor.dll (this is from the compiled VB.NET code)
  2. SystemMonitorPlugin.class (this was generated in Part 2)
  3. SystemMonitorJNI.dll (generated in Part3, it will be in the Debug folder of the project)

 

Now start up SageTV. After it launches, use Ctrl+Shift+F12 to launch the SageTV Studio (requires a licensed copy of SageTV). Then follow these steps

  1. Select File->Save As… and for the filename enter SageTV3VBTest.stv and then click Save
  2. Then select File->Import… and then navigate to the SystemMonitorPlugin.stvi file which was in the example files you downloaded, then click Open
  3. This will add a new Menu to your STV called "System Monitor Plugin". It also links that new Menu into the MainMenuTheme in the STV.
  4. Find the Menu "System Monitor Plugin" in the Studio and right-click on it and select Launch Menu.
  5. This should bring up the System Monitor menu in SageTV. The menu should refresh itself every 15 seconds. If it doesn't, you can try pressing F6 in the Studio window to refresh the menu.

 

That should do it! Now you've learned how to integrate a Visual Basic or C# .NET component into SageTV using the SageTV Studio. Best of luck to you!