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:
- Go to File->New->Project
- 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.
- In the Application Wizard dialog, select Application
Settings from the left side. Then on the right select DLL for Application
Type. Click Finish.
- 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.
- 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.
- 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
- 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.
- Right-click on the project in the Solution Explorer and
select Properties
- From the Configurations drop down, select All
Configurations
- Select Configuration Properties->C/C++->General from
the left side
- For 'Resolve #using References' enter: $(outdir)
- 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
- Select Configuration Properties->C/C++->Precompiled
Headers from the left side
- For 'Create/Use Precompiled Headers' select Not Using
Precompiled Headers
- Select Configuration Properties->Build
Events->Pre-Build Events from the left side
- For 'Command Line' enter the following: copy
"..\..\BasicSystemMonitor_Plugin\bin\BasicSystemMonitor.dll"
$(ConfigurationName)
- 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.
- Click on the Apply button, the properties dialog will
remain open.
- In the 'Solution Explorer', click on the
SystemMonitorJNI.cpp file. This will change the contents of the Properties
dialog.
- Select Configuration Properties->C/C++->General from
the left side
- For 'Compile as Managed' select Assembly Support (/clr)
- Click on Apply
- In the 'Solution Explorer', click on the ManagedToJava.cpp
file. This will change the contents of the Properties dialog.
- Select Configuration Properties->C/C++->General from
the left side
- For 'Compile as Managed' select Assembly Support (/clr)
- Click on Apply
- From the Configurations drop down, select Active(Debug)
- Select Configuration Properties->C/C++->Code
Generation from the left side
- For 'Basic Runtime Checks' select Default
- For 'Enable Minimal Rebuild' select No
- Select Configuration Properties->C/C++->General from
the left side
- For 'Debug Information Format' select Program Database
- 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.
- Visual C++ Toolkit 2003 - http://www.microsoft.com/downloads/details.aspx?FamilyId=272BE09D-40BB-49FD-9CB0-4BFA122FA91B&displaylang=en
- 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:
- BasicSystemMonitor.dll
- ManagedToJava.cpp
- ManagedToJava.h
- stdafx.h
- SystemMonitorJNI.cpp
- 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:
- BasicSystemMonitor.dll (this is from the compiled VB.NET
code)
- SystemMonitorPlugin.class (this was generated in Part 2)
- 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
- Select File->Save As… and for the filename enter
SageTV3VBTest.stv and then click Save
- Then select File->Import… and then navigate to the
SystemMonitorPlugin.stvi file which was in the example files you
downloaded, then click Open
- 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.
- Find the Menu "System Monitor Plugin" in the
Studio and right-click on it and select Launch Menu.
- 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!