Keep JVM alive with JNI

H

Herman

I have an application written in C that invokes a JVM and is using a
charting package written in java. I'm writing out png files from the
application. Everthing compiles fine and the C application will behave
correctly the first time through. But when I come back and try it
again it bombs with a segmentation fault. What I need to do is keep
the JVM alive when it is intially invoked. My program is a derivation
of the invoke.c that is in the JNI documentation on the java.sun.com
site. I've looked at the thread example in the JNI documentation but
I'm not sure that my application needs to multithreaded as much as it
needs to be reentrant and the JVM should be 'kept alive'. Any help
with this matter would be greatly appreciated.
 
G

Gordon Beaton

I have an application written in C that invokes a JVM and is using a
charting package written in java. I'm writing out png files from the
application. Everthing compiles fine and the C application will
behave correctly the first time through. But when I come back and
try it again it bombs with a segmentation fault. What I need to do
is keep the JVM alive when it is intially invoked.

What exactly do you mean by "come back and try it again"? Do you start
the C program a second time? If that causes a segmentation fault, then
I'd be looking for errors in the code, rather than a way to avoid
having to start it again.
My program is a derivation of the invoke.c that is in the JNI
documentation on the java.sun.com site. I've looked at the thread
example in the JNI documentation but I'm not sure that my
application needs to multithreaded as much as it needs to be
reentrant and the JVM should be 'kept alive'. Any help with this
matter would be greatly appreciated.

You don't need to do anything special. Once you've called
JNI_CreateJavaVM(), invoke whatever method you need to invoke. The JVM
doesn't terminate when the method returns, so you can invoke another
method later (in the same JVM) if you like. There's absolutely nothing
special you need to do.

Your description is rather vague, so it's difficult to give more
specific advice. Post a short, relevant code example.

/gordon
 
H

Herman

Gordon Beaton said:
What exactly do you mean by "come back and try it again"? Do you start
the C program a second time? If that causes a segmentation fault, then
I'd be looking for errors in the code, rather than a way to avoid
having to start it again.


You don't need to do anything special. Once you've called
JNI_CreateJavaVM(), invoke whatever method you need to invoke. The JVM
doesn't terminate when the method returns, so you can invoke another
method later (in the same JVM) if you like. There's absolutely nothing
special you need to do.

Your description is rather vague, so it's difficult to give more
specific advice. Post a short, relevant code example.

/gordon

Sorry for the vagueness. Here is a code snippet:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jni.h>
#include "idscharts.h"

#define USER_CLASSPATH ".";


JavaVM *jvm;
static jint res = -1;
JNIEnv *env;
static jclass cls;
static jmethodID mid;

int chartsX(CChartData *chartData,
CChartProps chartProps,
int numElements,
char *javaHome,
char *display,
char *chartWrapper,
char *pja,
char *jPowered)
{

jfieldID width, height, fname, type;

jstring jstr;
jclass stringClass, doubleClass;
jobjectArray segLabels, segValues;

int i;
char* label, element;
static char jarfiles[255] = "";
char classpath[255] = "-Djava.class.path=";
static char value[1024];
char* position;
int index;
static char* jh;



char javafont[100] = "-Djava.awt.fonts=";
char bootclasspath[100] = "-Xbootclasspath/a:";
static char pjadir[100];
char search_char = '/';

jh = javaHome;


/* cut off filename from directory. used for user.home JVM option */
position = strrchr(pja, '/');
index = pja - position;

if (index < 0 )
index *= -1;

strncpy(pjadir, pja, index);


strcat(jarfiles, chartWrapper);
strcat(jarfiles, ":");
strcat(jarfiles, jPowered);
strcat(classpath, jarfiles);



/* append the directory and filename to java -xbootclasspath
directive */
strcat(bootclasspath, pja);



/* Make string of where java fonts reside */
strcat(jh,"/lib/fonts");
strcat(javafont, jh);



#ifdef JNI_VERSION_1_4
JavaVMInitArgs vm_args;
JavaVMOption options[10];
options[0].optionString =
"-Djava.awt.headless=false";
options[1].optionString = bootclasspath;
options[2].optionString = "-Xms256m"; /* convert to variable if needs
to be dynamically adjusted */
options[3].optionString = "-Xmx512m";
options[4].optionString =
"-Dawt.toolkit=com.eteks.awt.PJAToolkit";
options[5].optionString =
"-Djava.awt.graphics=com.eteks.PJAGraphicEnvironment";
options[6].optionString =
"-Djava2d.font.usePlatformFont=false";
options[7].optionString = javafont;
options[8].optionString = pjadir;
options[9].optionString = classpath;
vm_args.version = 0x00010004;
vm_args.options = options;
vm_args.nOptions = 10;
vm_args.ignoreUnrecognized = JNI_TRUE;
/* Create the Java VM */


if (res < 0 ) {
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
}
else
{
res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
}

#else
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */


if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}

cls = (*env)->FindClass(env, "ChartWrapper");
if (cls == NULL) {
goto destroy;
}
cls = (*env)->NewWeakGlobalRef(env, cls);
/*DeleteLocalRef(cls); */


mid = (*env)->GetMethodID(env, cls, "setChartSettings",
"([Ljava/lang/String;[Ljava/lang/String;)V");
if (mid == NULL) {
goto destroy;
}

width = (*env)->GetFieldID(env, cls, "WIDTH", "I");
if (width == NULL) {
printf("Width - Didn't get it\n");
}
(*env)->SetIntField(env, cls, width, chartProps.width);


height = (*env)->GetFieldID(env, cls, "HEIGHT", "I");
if (height == NULL) {
goto destroy;
}
(*env)->SetIntField(env, cls, height, chartProps.height);

type = (*env)->GetFieldID(env, cls, "chartType", "I");
if (height == NULL) {
goto destroy;
}
(*env)->SetIntField(env, cls, type, chartProps.chartType);


stringClass = (*env)->FindClass(env, "java/lang/String");
jstr = (*env)->NewStringUTF(env, " ");

segLabels = (*env)->NewObjectArray (env, numElements ,
(*env)->FindClass(env, "java/lang/String"),jstr);


segValues = (*env)->NewObjectArray (env, numElements ,
(*env)->FindClass(env, "java/lang/String"),jstr);

for (i = 0; i < numElements; i++) {

label = chartData.label;
jstr = (*env)->NewStringUTF(env, label);
sprintf(value, "%01.2f", chartData.value);
(*env)->SetObjectArrayElement(env, segLabels, i, jstr);
jstr = (*env)->NewStringUTF(env, value);
(*env)->SetObjectArrayElement(env, segValues, i, jstr);
}


fname = (*env)->GetFieldID(env, cls, "chartFileName",
"Ljava/lang/String;");
if (fname == NULL) {
goto destroy;
}

jstr = (*env)->NewStringUTF(env, chartProps.filename);

(*env)->SetObjectField(env, cls, fname, jstr);

(*env)->CallVoidMethod(env, cls, mid, segLabels, segValues);

return 0;

destroy:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
(*jvm)->DestroyJavaVM(jvm);
}

void destroyJVM() {
(*jvm)->DestroyJavaVM(jvm);
}

What I'm trying to do is keep the JVM and the ChartWrapper class
persistent so that the JVM doesn't have to be intialized again when
the chartX method is called again. This program is built as a shared
object and the chartX method is called by a c application. So
basically it is c calling java. Hopefully this is a bit clearer.
Thanks for any advice you can lend.
 
H

Herman

Gordon Beaton said:
What exactly do you mean by "come back and try it again"? Do you start
the C program a second time? If that causes a segmentation fault, then
I'd be looking for errors in the code, rather than a way to avoid
having to start it again.


You don't need to do anything special. Once you've called
JNI_CreateJavaVM(), invoke whatever method you need to invoke. The JVM
doesn't terminate when the method returns, so you can invoke another
method later (in the same JVM) if you like. There's absolutely nothing
special you need to do.

Your description is rather vague, so it's difficult to give more
specific advice. Post a short, relevant code example.

/gordon

The same method chartX will be called over and over. What seems to
happen is that tbe program bombs when called again.
 
G

Gordon Beaton

The same method chartX will be called over and over. What seems to
happen is that tbe program bombs when called again.

Ok after looking at your code I can see what you are trying to do:

int chartX()
{
if (!jvm) {
start jvm
}
else {
AttachCurrentThread();
}

invoke(setChartSettings(someValues));

return 0;
}

First, I wonder if one of your method calls has failed and you have
invoked DestroyJavaVM() before returning, or if the caller has invoked
destroy_JVM()?

I don't think DestroyJavaVM() actually works, at any rate I have never
been successful in starting a second JVM within the same process.
Judging from similar questions in this and related newsgroups over the
years, I don't think anyone else has either (although I could be
mistaken). That is the first place I'd look.

Second, there may be some issues with references accumulating in the
calling scope. Normally, when calling from Java to C, any references
returned to C automatically become eligible for GC when the C method
returns. In your case, chartsX() is not called within the scope of a
JVM (the relationship is the opposite) so all of your references stay
alive even after the function returns. You need to manage all of the
references yourself with DeleteLocalRef() before returning. For that
reason, I don't think it's a good idea to make repeated calls to
FindClass("java/lang/String"), for example. The same is true of all of
the label and value Strings you create in the loop, as well as the
ObjectArray.

I'd suggest that you first try invoking a simple static method that
doesn't require you to create any objects on each call, in order to
eliminate those issues from the equation. Make sure you are able to
invoke methods on the JVM the second and subsequent times, then
replace that method call with your call to setChartSettings().

Some additional points...

- calling DestroyJavaVM() doesn't change the value of res, which you
use to test whether to start the JVM next time. In fact I'd base the
test on the value of the jvm pointer itself, not res. Make sure you
set jvm to NULL after calling DestroyJavaVM() so that the second
call to your function doesn't attempt to invoke a method on a
non-existant JVM. But again, I don't think you can rely on
restarting the JVM after DestroyJavaVM() has been called.

- much of the code prior to starting the JVM is done on every call to
chartsX(), but is only necessary when you actually invoke
JNI_CreateJavaVM() the first time. Put all of that code inside the
conditional block. Or preferably, break it into a separate function
to start the JVM. Just test jvm in chartsX() and invoke the function
if necesary. Several of the currently global variables should be
local to that function.

- are you sure that the character arrays jarfiles, classpath,
javafont, bootclasspath, pjadir etc, are long enough to contain the
data you write to them? Perhaps you are overwriting the bounds of
one of these.

- it is a lot easier to get and set values via method arguments than
through reflection. Consider adding one or more helper methods to
ChartWrapper so you don't need to make all those calls to
GetFieldID() and SetXXField(),
e.g. setChartProps(name, type, width, height).

/gordon
 
H

Herman

Gordon Beaton said:
Ok after looking at your code I can see what you are trying to do:

int chartX()
{
if (!jvm) {
start jvm
}
else {
AttachCurrentThread();
}

invoke(setChartSettings(someValues));

return 0;
}

First, I wonder if one of your method calls has failed and you have
invoked DestroyJavaVM() before returning, or if the caller has invoked
destroy_JVM()?

I don't think DestroyJavaVM() actually works, at any rate I have never
been successful in starting a second JVM within the same process.
Judging from similar questions in this and related newsgroups over the
years, I don't think anyone else has either (although I could be
mistaken). That is the first place I'd look.

Second, there may be some issues with references accumulating in the
calling scope. Normally, when calling from Java to C, any references
returned to C automatically become eligible for GC when the C method
returns. In your case, chartsX() is not called within the scope of a
JVM (the relationship is the opposite) so all of your references stay
alive even after the function returns. You need to manage all of the
references yourself with DeleteLocalRef() before returning. For that
reason, I don't think it's a good idea to make repeated calls to
FindClass("java/lang/String"), for example. The same is true of all of
the label and value Strings you create in the loop, as well as the
ObjectArray.

I'd suggest that you first try invoking a simple static method that
doesn't require you to create any objects on each call, in order to
eliminate those issues from the equation. Make sure you are able to
invoke methods on the JVM the second and subsequent times, then
replace that method call with your call to setChartSettings().

Some additional points...

- calling DestroyJavaVM() doesn't change the value of res, which you
use to test whether to start the JVM next time. In fact I'd base the
test on the value of the jvm pointer itself, not res. Make sure you
set jvm to NULL after calling DestroyJavaVM() so that the second
call to your function doesn't attempt to invoke a method on a
non-existant JVM. But again, I don't think you can rely on
restarting the JVM after DestroyJavaVM() has been called.

- much of the code prior to starting the JVM is done on every call to
chartsX(), but is only necessary when you actually invoke
JNI_CreateJavaVM() the first time. Put all of that code inside the
conditional block. Or preferably, break it into a separate function
to start the JVM. Just test jvm in chartsX() and invoke the function
if necesary. Several of the currently global variables should be
local to that function.

- are you sure that the character arrays jarfiles, classpath,
javafont, bootclasspath, pjadir etc, are long enough to contain the
data you write to them? Perhaps you are overwriting the bounds of
one of these.

- it is a lot easier to get and set values via method arguments than
through reflection. Consider adding one or more helper methods to
ChartWrapper so you don't need to make all those calls to
GetFieldID() and SetXXField(),
e.g. setChartProps(name, type, width, height).

/gordon

Good suggestions - Thanks.

Now I've made the following changes - here is the code snippet ..

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jni.h>
#include "idscharts.h"

#define USER_CLASSPATH ".";

void destroyJVM();
int invokeJVM(char* bootclasspath, char* javafont, char* pjadir, char*
classpath);
void doChart(CChartData *chartData, CChartProps chartProps, int
numElements);
void cleanup(jstring jstr,jobjectArray segLabels,jobjectArray
segValues);

JavaVM *jvm;
JNIEnv *env;
jclass Global_cls;

/* Cache variables, method ids and field ids */
static jmethodID mid;
static jfieldID width;
static jfieldID height;
static jfieldID fname;
static jfieldID type;
static jclass stringClass;

int chartsX(CChartData *chartData,
CChartProps chartProps,
int numElements,
char *javaHome,
char *display,
char *chartWrapper,
char *pja,
char *jPowered)
{
char jarfiles[255] = "";
char classpath[1024] = "-Djava.class.path=";
char* jh;
char* position;
char javafont[255] = "-Djava.awt.fonts=";
char bootclasspath[255] = "-Xbootclasspath/a:";
char pjadir[255];
char search_char = '/';
int index, result;

puts("In top of code");

printf("The pja file: %s\n", pja);

jh = javaHome;


/* cut off filename from directory. used for user.home JVM option */
position = strrchr(pja, '/');
index = pja - position;

if (index < 0 )
index *= -1;

printf("This is the position: %d\n", index);
strncpy(pjadir, pja, index);

printf("%s\n", pja);
printf("%s\n", pjadir);

strcat(jarfiles, chartWrapper);
strcat(jarfiles, ":");
strcat(jarfiles, jPowered);
strcat(classpath, jarfiles);

/*printf("in the so file\n");*/
printf("%s\n", classpath);


/* append the directory and filename to java -xbootclasspath
directive */
strcat(bootclasspath, pja);

printf("%s\n", pjadir);


/* Make string of where java fonts reside */
strcat(jh,"/lib/fonts");
puts(jh);
strcat(javafont, jh);
printf("%s\n", javafont);

/* Test to see if the VM is alive */
result = invokeJVM(bootclasspath, javafont, pjadir, classpath);

if ( result < 0 )
{
destroyJVM();
}
else
{
doChart(chartData, chartProps, numElements);
}

return result;
}


int invokeJVM(char* bootclasspath,
char* javafont,
char* pjadir,
char* classpath)
{
static jint res;

if(!jvm) {
puts("No active VM ...");

#ifdef JNI_VERSION_1_4
JavaVMInitArgs vm_args;
JavaVMOption options[10];

options[0].optionString =
"-Djava.awt.headless=false";
options[1].optionString = bootclasspath;
options[2].optionString = "-Xms256m"; /* convert to variable if
needs to be dynamically adjusted */
options[3].optionString = "-Xmx512m";
options[4].optionString =
"-Dawt.toolkit=com.eteks.awt.PJAToolkit";
options[5].optionString =
"-Djava.awt.graphics=com.eteks.PJAGraphicEnvironment";
options[6].optionString =
"-Djava2d.font.usePlatformFont=false";
options[7].optionString = javafont;
options[8].optionString = pjadir;
options[9].optionString = classpath;
vm_args.version = 0x00010004;
vm_args.options = options;
vm_args.nOptions = 10;
vm_args.ignoreUnrecognized = JNI_TRUE;
/* Create the Java VM */
printf("%s\n", "Trying to create VM ...");
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else
JDK1_1InitArgs vm_args;
char classpath[1024];

vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */

printf(" In .so chart function \n");

if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
}
else
{
puts("VM Active ...");
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
}
return res;
}


void doChart(CChartData *chartData,
CChartProps chartProps,
int numElements)
{
jstring jstr;
jobjectArray segLabels;
jobjectArray segValues;
char* label;
char* element;
char value[1024];

static jclass cls;

int i;

if (!Global_cls) {
cls = (*env)->FindClass(env, "ChartWrapper");
if (cls == NULL) {
goto destroy;
}
printf("Class exists\n");

/* Use weak global ref to allow C class to be unloaded */
Global_cls = (*env)->NewWeakGlobalRef(env, cls);
if (Global_cls== NULL) {
printf("%d\n", JNI_ERR);
}
(*env)->DeleteLocalRef(env, cls);
}

if (!mid) {
mid = (*env)->GetMethodID(env, Global_cls,
"setChartSettings", "([Ljava/lang/String;[Ljava/lang/String;)V");
if (mid == NULL) {
goto destroy;
}
printf("setChartSettings() Method exist\n");

width = (*env)->GetFieldID(env, Global_cls, "WIDTH", "I");
if (width == NULL) {
printf("Width - Didn't get it\n");
}

height = (*env)->GetFieldID(env, Global_cls, "HEIGHT", "I");
if (height == NULL) {
printf("Height - Didn't get it\n");
}

type = (*env)->GetFieldID(env, Global_cls, "chartType", "I");
if (height == NULL) {
printf("Type - Didn't get it\n");
}

stringClass = (*env)->FindClass(env, "java/lang/String");
}

jstr = (*env)->NewStringUTF(env, " ");


/* Set individual fields in class. May change this implementation...
*/
(*env)->SetIntField(env, Global_cls, width, chartProps.width);
(*env)->SetIntField(env, Global_cls, height, chartProps.height);
(*env)->SetIntField(env, Global_cls, type, chartProps.chartType);


segLabels = (*env)->NewObjectArray (env, numElements ,stringClass,
jstr);
segValues = (*env)->NewObjectArray (env, numElements ,stringClass,
jstr);

for (i = 0; i < numElements; i++) {
label = chartData.label;
jstr = (*env)->NewStringUTF(env, label);
sprintf(value, "%01.2f", chartData.value);
(*env)->SetObjectArrayElement(env, segLabels, i, jstr);
jstr = (*env)->NewStringUTF(env, value);
(*env)->SetObjectArrayElement(env, segValues, i, jstr);
}


fname = (*env)->GetFieldID(env, Global_cls, "chartFileName",
"Ljava/lang/String;");
if (fname == NULL) {
printf("filename - Didn't get it\n");
}
/*puts(chartProps.filename);*/

jstr = (*env)->NewStringUTF(env, chartProps.filename);

/*printf("Try to assign filename\n");*/
(*env)->SetObjectField(env, Global_cls, fname, jstr);

(*env)->CallVoidMethod(env, Global_cls, mid, segLabels,
segValues);

/* Try to cleanup after ourselves*/
cleanup(width, height, fname, type, jstr, segLabels, segValues);

destroy:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
//(*jvm)->DestroyJavaVM(jvm);
}

void cleanup(jstring jstr,jobjectArray segLabels,jobjectArray
segValues)
{
(*env)->DeleteLocalRef(env, jstr);
(*env)->DeleteLocalRef(env, segLabels);
(*env)->DeleteLocalRef(env, segValues);

}


void destroyJVM() {
(*jvm)->DestroyJavaVM(jvm);
}

I'm going to add the helper methods as you suggested. Obviously it's
not in the listing above.

What's happening now is it locating the VM, but it crashes after the
second time through. Probably still not releasing all necessary
resources??

One thing that might be a problem is the for loop; I'm getting the
values of the elements from a C structure and an array of structs, so
I think I'm going to have to use some kind of loop. I then pass two
String Arrays to java that will populate a Vector. If there is a
better way of accomplishing this, please enlighten me.

Thanks in advanced
 
H

Herman

Gordon Beaton said:
Ok after looking at your code I can see what you are trying to do:

int chartX()
{
if (!jvm) {
start jvm
}
else {
AttachCurrentThread();
}

invoke(setChartSettings(someValues));

return 0;
}

First, I wonder if one of your method calls has failed and you have
invoked DestroyJavaVM() before returning, or if the caller has invoked
destroy_JVM()?

I don't think DestroyJavaVM() actually works, at any rate I have never
been successful in starting a second JVM within the same process.
Judging from similar questions in this and related newsgroups over the
years, I don't think anyone else has either (although I could be
mistaken). That is the first place I'd look.

Second, there may be some issues with references accumulating in the
calling scope. Normally, when calling from Java to C, any references
returned to C automatically become eligible for GC when the C method
returns. In your case, chartsX() is not called within the scope of a
JVM (the relationship is the opposite) so all of your references stay
alive even after the function returns. You need to manage all of the
references yourself with DeleteLocalRef() before returning. For that
reason, I don't think it's a good idea to make repeated calls to
FindClass("java/lang/String"), for example. The same is true of all of
the label and value Strings you create in the loop, as well as the
ObjectArray.

I'd suggest that you first try invoking a simple static method that
doesn't require you to create any objects on each call, in order to
eliminate those issues from the equation. Make sure you are able to
invoke methods on the JVM the second and subsequent times, then
replace that method call with your call to setChartSettings().

Some additional points...

- calling DestroyJavaVM() doesn't change the value of res, which you
use to test whether to start the JVM next time. In fact I'd base the
test on the value of the jvm pointer itself, not res. Make sure you
set jvm to NULL after calling DestroyJavaVM() so that the second
call to your function doesn't attempt to invoke a method on a
non-existant JVM. But again, I don't think you can rely on
restarting the JVM after DestroyJavaVM() has been called.

- much of the code prior to starting the JVM is done on every call to
chartsX(), but is only necessary when you actually invoke
JNI_CreateJavaVM() the first time. Put all of that code inside the
conditional block. Or preferably, break it into a separate function
to start the JVM. Just test jvm in chartsX() and invoke the function
if necesary. Several of the currently global variables should be
local to that function.

- are you sure that the character arrays jarfiles, classpath,
javafont, bootclasspath, pjadir etc, are long enough to contain the
data you write to them? Perhaps you are overwriting the bounds of
one of these.

- it is a lot easier to get and set values via method arguments than
through reflection. Consider adding one or more helper methods to
ChartWrapper so you don't need to make all those calls to
GetFieldID() and SetXXField(),
e.g. setChartProps(name, type, width, height).

/gordon


It seems like its doesn't know what to do after attaching to the
thread and it core dumps.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,764
Messages
2,569,564
Members
45,039
Latest member
CasimiraVa

Latest Threads

Top