This patch should be applied to the "ajuba-tclblend-contract-2000-08-01-branch". The patch includes the following: - fix bugs in tcl.lang.Notifier - make TclBlend work with the Jacl shell - add mutex for multi thread TclBlend initialization - add (Win) makefile targets for testing TclBlend in JVM This patch together with Mo's changes on removing the global lock and making JNIEnv thread local make TclBlend almost thread safe. I say "almost" because we still need to make one last change to the Java side of TclBlend to make the Notifier class per Interp. I outlined the various initialization paths the TclBlend can go through and how the mutex protects these situations in the following document, item 3 and 4. Your comments and questions are welcome. http://www-cs-students.stanford.edu/~jwu/blendchanges.txt -- Jiang Wu [EMAIL PROTECTED]
Index: ChangeLog =================================================================== RCS file: /cvsroot/tcljava/ChangeLog,v retrieving revision 1.49.2.2 diff -b -u -r1.49.2.2 ChangeLog --- ChangeLog 2000/07/30 07:17:08 1.49.2.2 +++ ChangeLog 2000/08/05 23:25:45 @@ -1,3 +1,24 @@ +2000-08-03 Jiang Wu <[EMAIL PROTECTED]> + * src/tclblend/tcl/lang/Notifier.java: + Fix the following Notifier bugs: + - infinite loop when doOneEvent is called recursively + - synchronization deadlock between serviceEvent and queueEvent + - deleteEvent() does not work properly + * src/tclblend/tcl/lang/Util.java: + * src/tclblend/tcl/lang/Interp.java: + Added the necessary methods for tcl.lang.Shell. + * src/native/javaCmd.c: + * src/native/javaInterp.c: + Added a global mutex to protect TclBlend initialization from + multiple C or Java threads. + * win/makefile.vc + Modified the Win makefile to build the Jacl Shell for tclblend. + Added a 'blendsh.install' target to install a 'blendsh.bat' that + can be used to start a Java shell running TclBlend. Added a + 'test_jvmblend.exec' target to run the TclBlend test suite using + JVM loading TclBlend. + + 2000-07-30 Mo DeJong <[EMAIL PROTECTED]> * src/native/java.h: Index: src/native/javaCmd.c =================================================================== RCS file: /cvsroot/tcljava/src/native/javaCmd.c,v retrieving revision 1.9.2.1 diff -b -u -r1.9.2.1 javaCmd.c --- javaCmd.c 2000/07/30 07:17:08 1.9.2.1 +++ javaCmd.c 2000/08/05 23:25:48 @@ -94,6 +94,22 @@ static int initialized_javaVM = 0; /* + * Declare a global mutex to protect the creation and initialization of the + * JVM from mutiple threads. This mutex is used in conjunction with the + * 'initialized_javaVM' flag. This mutex is used in javaCmd.c as well as + * javaInterp.c. + * + * FIXME: don't want to use the flag TCL_THREADS explicitly. This may be + * better if in the future the same TclBlend binary can be made to work with + * both threaded and non-threaded Tcl libraries. For now, we will use accessor + * functions lockJVMInitMutex() and unlockJVMInitMutex(). + */ +TCL_DECLARE_MUTEX(javaVM_init_mutex) + +TCLBLEND_EXTERN void LockJVMInitMutex(); +TCLBLEND_EXTERN void UnlockJVMInitMutex(); + +/* * The following array contains the class names and jclass pointers for * all of the classes used by this module. It is used to initialize * the java structure's jclass slots. @@ -242,6 +258,13 @@ fprintf(stderr, "Tcl Blend debug: CLASSPATH is \"%s\"\n", getenv("CLASSPATH")); #endif /* TCLBLEND_DEBUG */ + /* + * Calling JavaGetEnv() and JavaSetupJava() should be atomic because these + * two functions may alter global data. Lock the JVM init mutex to create + * a critical section. + */ + LockJVMInitMutex(); + env = JavaGetEnv(interp); /* @@ -254,6 +277,7 @@ fprintf(stderr, "Tcl Blend debug: JVM init failed in javaCmd.c:\n"); #endif /* TCLBLEND_DEBUG */ + UnlockJVMInitMutex(); return TCL_ERROR; } @@ -275,11 +299,18 @@ if (JavaSetupJava(env, interp) != TCL_OK) { + UnlockJVMInitMutex(); return TCL_ERROR; } } /* + * End critical section. From this point on, the code is thread + * specific. + */ + UnlockJVMInitMutex(); + + /* * Allocate a new Interp instance to wrap this interpreter. */ @@ -405,7 +436,7 @@ } /* FIXME : we need to put a mutex around this create JVM/attach step */ - + /* ^^^^^ -- no need for mutex here, use mutex in the caller function */ #ifdef TCLBLEND_DEBUG fprintf(stderr, "Tcl Blend debug: init 1.2 JVM in javaCmd.c\n"); #endif /* TCLBLEND_DEBUG */ @@ -573,7 +604,7 @@ } /* FIXME: need to put a mutex around this create/attach step */ - + /* ^^^^^ -- no need for mutex here, use mutex in the caller function */ #ifdef TCLBLEND_DEBUG fprintf(stderr, "Tcl Blend debug: init 1.1 JVM in javaCmd.c: \n"); #endif /* TCLBLEND_DEBUG */ @@ -846,6 +877,7 @@ /* FIXME : we need to put a mutex around this test/set */ + /* ^^^^^ -- no need for mutex here, use mutex in the caller function */ if (initialized_javaVM) { return TCL_OK; } @@ -1122,3 +1154,45 @@ return buf; } + +/* + *---------------------------------------------------------------------- + * + * LockJVMInitMutex -- + * + * Acquire the 'init_javaVM_mutex'. + * + * Results: + * None. + * + * Side effects: + * Maybe block the caller until the mutex is available. + * + *---------------------------------------------------------------------- + */ +TCLBLEND_EXTERN void +LockJVMInitMutex() +{ + Tcl_MutexLock(&javaVM_init_mutex); +} + +/* + *---------------------------------------------------------------------- + * + * UnlockJVMInitMutex -- + * + * Release the 'init_javaVM_mutex'. + * + * Results: + * None. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ +TCLBLEND_EXTERN void +UnlockJVMInitMutex() +{ + Tcl_MutexUnlock(&javaVM_init_mutex); +} Index: src/native/javaInterp.c =================================================================== RCS file: /cvsroot/tcljava/src/native/javaInterp.c,v retrieving revision 1.7.2.1 diff -b -u -r1.7.2.1 javaInterp.c --- javaInterp.c 2000/07/30 07:17:09 1.7.2.1 +++ javaInterp.c 2000/08/05 23:25:51 @@ -28,6 +28,12 @@ } JavaTraceInfo; /* + * This file wants to use lockJVMInitMutex() and unlockJVMInitMutex(). + */ +TCLBLEND_EXTERN void LockJVMInitMutex(); +TCLBLEND_EXTERN void UnlockJVMInitMutex(); + +/* * Declaractions for functions used only in this file. */ @@ -102,6 +108,14 @@ Tcl_Interp *interp; interp = Tcl_CreateInterp(); + + /* + * JavaSetupJava() should be executed atomically because it may + * alter global data. Use the JVM init mutex to create a critical + * section. + */ + LockJVMInitMutex(); + if (JavaSetupJava(env, interp) != TCL_OK) { jclass err = (*env)->FindClass(env, "tcl/lang/TclRuntimeError"); if (err) { @@ -112,6 +126,11 @@ } else { *(Tcl_Interp**)&lvalue = interp; } + + /* + * End critical section. + */ + UnlockJVMInitMutex(); return lvalue; } Index: src/tclblend/tcl/lang/Interp.java =================================================================== RCS file: /cvsroot/tcljava/src/tclblend/tcl/lang/Interp.java,v retrieving revision 1.16 diff -b -u -r1.16 Interp.java --- Interp.java 2000/07/30 02:37:18 1.16 +++ Interp.java 2000/08/05 23:25:53 @@ -318,6 +318,37 @@ /* *---------------------------------------------------------------------- * + * setVar -- + * + * Set the value of a variable. + * + * Results: + * Returns the new value of the variable. + * + * Side effects: + * May trigger traces. + * + *---------------------------------------------------------------------- + */ + +public final TclObject +setVar( + String name, // Name of variable, array, or array element + // to set. + String value, // New value for variable. + int flags) // Various flags that tell how to set value: + // any of GLOBAL_ONLY, NAMESPACE_ONLY, + // APPEND_VALUE, LIST_ELEMENT, or + // LEAVE_ERR_MSG. +throws + TclException +{ + return setVar(name, TclString.newInstance(value), flags); +} + +/* + *---------------------------------------------------------------------- + * * getVar -- * * Get the value of a variable. @@ -819,8 +850,23 @@ throws TclException // A standard Tcl exception. { + // Append the script to the event list by calling "history add <script>". + // We call the eval method with the command of type TclObject, so that + // we don't have to deal with funny chars ("{}[]$\) in the script. + + try { + TclObject cmd = TclList.newInstance(); + TclList.append(this, cmd, TclString.newInstance("history")); + TclList.append(this, cmd, TclString.newInstance("add")); + TclList.append(this, cmd, script); + eval(cmd, 0); + } catch (Exception e) {} + + // Finally evaluate the script. + + eval(script, flags); + // FIXME : need native implementation - throw new TclRuntimeError("Not implemented yet."); } /* @@ -846,8 +892,11 @@ throws TclException { + // use the source command to evalFile + eval("source {" + s + "}", 0); + // FIXME : need implementation - throw new TclRuntimeError("Not implemented yet."); + // throw new TclRuntimeError("Not implemented yet."); } /* @@ -1031,6 +1080,42 @@ public native void addErrorInfo( String message); // Message to add to errorInfo + +/* + *---------------------------------------------------------------------- + * + * updateReturnInfo -- + * + * This internal method is used by various parts of the Jacl + * interpreter when a TclException of TCL.RETURN is received. + * + * This method is provided here for compatibility with Jacl Interp class + * methods. This method should never be reached in TclBlend. + * + * Results: + * The return value is the true completion code to use for + * the Tcl procedure, instead of TCL.RETURN. It's the same + * value that was given to the "return -code" option. + * + * If an internal value of TCL.OK is returned, it means than the + * caller of this method should ignore any TclException that it has + * just received and continue execution. + * + * Side effects: + * The errorInfo and errorCode variables may get modified. + * + *---------------------------------------------------------------------- + */ + +protected int +updateReturnInfo() +{ + int code = TCL.ERROR; + + setErrorCode(TclInteger.newInstance(code)); + addErrorInfo("Unexpected call to updateReturnInfo() in TclBlend."); + return code; +} /* *---------------------------------------------------------------------- Index: src/tclblend/tcl/lang/Notifier.java =================================================================== RCS file: /cvsroot/tcljava/src/tclblend/tcl/lang/Notifier.java,v retrieving revision 1.2 diff -b -u -r1.2 Notifier.java --- Notifier.java 2000/01/25 03:42:25 1.2 +++ Notifier.java 2000/08/05 23:25:55 @@ -39,7 +39,7 @@ * interpreter (thread). */ -public class Notifier { +public class Notifier implements EventDeleter { private static Notifier globalNotifier; @@ -65,6 +65,13 @@ int refCount; +// Mutex used to protect concurrent access to the internals of this Notifier +// object. For example, the queueing and dequeueing of objects from the +// event list. + +private final Object notifierMutex = new Object(); + + /* *---------------------------------------------------------------------- * @@ -127,7 +134,7 @@ * * Increment the reference count of the notifier. The notifier will * be kept in the notifierTable (and alive) as long as its reference - * count is greater than zero. + * count is greater than zero. This method is concurrent safe. * * Results: * None. @@ -138,13 +145,16 @@ *---------------------------------------------------------------------- */ -public synchronized void +public void preserve() { + synchronized (notifierMutex) { if (refCount < 0) { - throw new TclRuntimeError("Attempting to preserve a freed Notifier"); + throw new TclRuntimeError( + "Attempting to preserve a freed Notifier"); } ++refCount; + } } /* @@ -153,7 +163,8 @@ * release -- * * Decrement the reference count of the notifier. The notifier will - * be free when its refCount goes from one to zero. + * be free when its refCount goes from one to zero. This method is + * concurrent safe. * * Results: * None. @@ -165,9 +176,10 @@ *---------------------------------------------------------------------- */ -public synchronized void +public void release() { + synchronized (notifierMutex) { if ((refCount == 0) && (primaryThread != null)) { throw new TclRuntimeError( "Attempting to release a Notifier before it's preserved"); @@ -181,6 +193,7 @@ globalNotifier = null; dispose(); } + } } /* @@ -193,7 +206,8 @@ * Events inserted before the marker will be processed in * first-in-first-out order, but before any events inserted at * the tail of the queue. Events inserted at the head of the - * queue will be processed in last-in-first-out order. + * queue will be processed in last-in-first-out order. This method is + * concurrent safe. * * Results: * None. @@ -206,7 +220,7 @@ *---------------------------------------------------------------------- */ -public synchronized void +public void queueEvent( TclEvent evt, // The event to put in the queue. int position) // One of TCL.QUEUE_TAIL, @@ -214,9 +228,9 @@ { evt.notifier = this; + synchronized (notifierMutex) { if (position == TCL.QUEUE_TAIL) { // Append the event on the end of the queue. - evt.next = null; if (firstEvent == null) { @@ -227,7 +241,6 @@ lastEvent = evt; } else if (position == TCL.QUEUE_HEAD) { // Push the event on the head of the queue. - evt.next = firstEvent; if (firstEvent == null) { lastEvent = evt; @@ -236,7 +249,6 @@ } else if (position == TCL.QUEUE_MARK) { // Insert the event after the current marker event and advance // the marker to the new event. - if (markerEvent == null) { evt.next = firstEvent; firstEvent = evt; @@ -250,10 +262,10 @@ } } else { // Wrong flag. - throw new TclRuntimeError("wrong position \"" + position + "\", must be TCL.QUEUE_HEAD, TCL.QUEUE_TAIL or TCL.QUEUE_MARK"); } + } if (Thread.currentThread() != primaryThread) { alertNotifier(); @@ -267,7 +279,8 @@ * * Calls an EventDeleter for each event in the queue and deletes * those for which deleter.deleteEvent() returns 1. Events - * for which the deleter returns 0 are left in the queue. + * for which the deleter returns 0 are left in the queue. This + * method is concurrent safe. * * Results: * None. @@ -278,13 +291,14 @@ *---------------------------------------------------------------------- */ -public synchronized void +public void deleteEvents( EventDeleter deleter) // The deleter that checks whether an event // should be removed. { TclEvent evt, prev; + synchronized (notifierMutex) { for (prev = null, evt = firstEvent; evt != null; evt = evt.next) { if (deleter.deleteEvent(evt) == 1) { if (firstEvent == evt) { @@ -292,16 +306,49 @@ if (evt.next == null) { lastEvent = null; } + if (markerEvent == evt) { + markerEvent = null; + } } else { prev.next = evt.next; + if (evt.next == null) { + lastEvent = prev; } if (markerEvent == evt) { - markerEvent = null; + markerEvent = prev; + } } } else { prev = evt; } } + } +} + +/* + *---------------------------------------------------------------------- + * + * deleteEvent -- + * + * This method is required for this class being an EventDeleter. + * Checks the given event to see if it is already processed. + * This method together with 'deleteEvents' are used by serviceEvent to + * remove an event that is just processed in a concurrent safe manor. + * + * Results: + * Returns 1 if the given event is processed, 0 otherwise. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +public int +deleteEvent( + TclEvent evt) { // Check whether this event should be removed. + return ((evt.isProcessing == true) && + (evt.isProcessed == true)) ? 1 : 0; } /* @@ -309,7 +356,8 @@ * * serviceEvent -- * - * Process one event from the event queue. + * Process one event from the event queue. This method is concurrent + * safe and can be reentered recursively. * * Results: * The return value is 1 if the procedure actually found an event @@ -322,8 +370,7 @@ * *---------------------------------------------------------------------- */ - -synchronized int +int serviceEvent( int flags) // Indicates what events should be processed. // May be any combination of TCL.WINDOW_EVENTS @@ -343,7 +390,8 @@ // Loop through all the events in the queue until we find one // that can actually be handled. - for (evt = firstEvent; evt != null; evt = evt.next) { + evt = null; + while ((evt = getAvailableEvent(evt)) != null) { // Call the handler for the event. If it actually handles the // event then free the storage for the event. There are two // tricky things here, both stemming from the fact that the event @@ -357,37 +405,18 @@ // change almost arbitrarily while handling the event, so we // can't depend on pointers found now still being valid when // the handler returns. - + synchronized (evt) { boolean b = evt.isProcessing; evt.isProcessing = true; if ((b == false) && (evt.processEvent(flags) != 0)) { evt.isProcessed = true; if (evt.needsNotify) { - synchronized(evt) { evt.notifyAll(); } - } - if (firstEvent == evt) { - firstEvent = evt.next; - if (evt.next == null) { - lastEvent = null; - } - if (markerEvent == evt) { - markerEvent = null; - } - } else { - for (prev = firstEvent; prev.next != evt; prev = prev.next) { - // Empty loop body. - } - prev.next = evt.next; - if (evt.next == null) { - lastEvent = prev; - } - if (markerEvent == evt) { - markerEvent = prev; - } - } + + deleteEvents(this); // this takes care of deletion of the + // just processed event return 1; } else { // The event wasn't actually handled, so we have to @@ -395,19 +424,59 @@ // attempted again. evt.isProcessing = b; - } // The handler for this event asked to defer it. Just go on to - // the next event. - - continue; + // the next event. we will try to find another event to + // process when the while loop continues + } + } } + return 0; } /* *---------------------------------------------------------------------- * + * getAvailableEvent -- + * + * Search through the internal event list to find the first event + * that is has not being processed AND the event is not equal to the given + * 'skipEvent'. This method is concurrent safe. + * + * Results: + * The return value is a pointer to the first found event that can be + * processed. If no event is found, this method returns null. + * + * Side effects: + * This method synchronizes on the 'notifierMutex', which will block any + * other thread from adding or removing events from the event queue. + * + *---------------------------------------------------------------------- + */ + +private TclEvent +getAvailableEvent( + TclEvent skipEvent) // Indicates that the given event should not + // be returned. This argument can be null. +{ + TclEvent evt; + + synchronized(notifierMutex) { + for (evt = firstEvent ; evt != null ; evt = evt.next) { + if ((evt.isProcessing == false) && + (evt.isProcessed == false) && + (evt != skipEvent)) { + return evt; + } + } + } + return null; +} + +/* + *---------------------------------------------------------------------- + * * doOneEvent -- * * Process a single event of some sort. If there's no work to @@ -503,7 +572,8 @@ * * hasEvents -- * - * Check to see if there are events waiting to be processed. + * Check to see if there are events waiting to be processed. This + * method is concurrent safe. * * Results: * Returns true if there are events on the Notifier queue. @@ -514,10 +584,12 @@ *---------------------------------------------------------------------- */ -private final synchronized boolean +private final boolean hasEvents() { - return (firstEvent != null); + boolean result = (getAvailableEvent(null) != null); + + return result; } } // end Notifier Index: src/tclblend/tcl/lang/Util.java =================================================================== RCS file: /cvsroot/tcljava/src/tclblend/tcl/lang/Util.java,v retrieving revision 1.1.1.1 diff -b -u -r1.1.1.1 Util.java --- Util.java 1998/10/14 21:09:10 1.1.1.1 +++ Util.java 2000/08/05 23:25:55 @@ -59,6 +59,80 @@ /* *---------------------------------------------------------------------- * + * isUnix -- + * + * Returns true if running on a Unix platform. + * + * Results: + * Returns a boolean. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +final static boolean +isUnix() { + if (isMac() || isWindows()) { + return false; + } + return true; +} + +/* + *---------------------------------------------------------------------- + * + * isMac -- + * + * Returns true if running on a Mac platform. + * + * Results: + * Returns a boolean. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +final static boolean +isMac() { + String os = System.getProperty("os.name"); + if (os.toLowerCase().startsWith("mac")) { + return true; + } + return false; +} + +/* + *---------------------------------------------------------------------- + * + * isWindows -- + * + * Returns true if running on a Windows platform. + * + * Results: + * Returns a boolean. + * + * Side effects: + * None. + * + *---------------------------------------------------------------------- + */ + +final static boolean +isWindows() { + String os = System.getProperty("os.name"); + if (os.toLowerCase().startsWith("win")) { + return true; + } + return false; +} + +/* + *---------------------------------------------------------------------- + * * printDouble -- * * Returns the string form of a double. The exact formatting Index: win/makefile.vc =================================================================== RCS file: /cvsroot/tcljava/win/makefile.vc,v retrieving revision 1.27.2.1 diff -b -u -r1.27.2.1 makefile.vc --- makefile.vc 2000/07/30 04:33:15 1.27.2.1 +++ makefile.vc 2000/08/05 23:26:00 @@ -350,6 +350,22 @@ exit << +test_jvmblend.exec: test_tclblend.check + cd $(BUILD_DIR) + set CLASSPATH=$(TEST_CLASSPATH) + set PATH=$(TCL_DIR)\bin;$(BUILD_DIR);$(JAVA_RUNTIME_LIBS);$(PATH) + set TCL_LIBRARY=$(TCL_DIR)\lib\tcl8.3 + $(JAVA) $(JAVA_FLAGS) \ + -DBUILD_DIR=$(BUILD_DIR) -DJAVA=$(JAVA) tcl.lang.Shell << + set auto_path [concat [pwd] [set auto_path]] + cd {$(TOP_DIR)\tests} + puts "pwd is [pwd]" + puts "CLASSPATH is [set env(CLASSPATH)]" + puts "auto_path is [set auto_path]" + source all + puts "Tests done." + exit +<< #---------------------------------------------------------------------- # Running the Jacl test suite @@ -567,7 +583,8 @@ cd $(TCLBLEND_SRC_DIR) set CLASSPATH=$(TCLBLEND_CLASSPATH) $(JAVAC) $(JAVAC_FLAGS) \ - -d $(TCLBLEND_BUILD_DIR) tcl\lang\*.java + -d $(TCLBLEND_BUILD_DIR) tcl\lang\*.java \ + $(JACL_SRC_DIR)\tcl\lang\Shell.java tclblend.jar: tclblend.build cd $(TCLBLEND_BUILD_DIR) @@ -601,6 +618,19 @@ -@mkdir $(TCL_DIR)\lib\xputils -@$(COPYDIR) $(BUILD_DIR)\xputils $(TCL_DIR)\lib\xputils +# +# temp target for installing a blendsh.bat +# +blendsh.install: + -@echo Installing blendsh.bat in $(BIN_INSTALL_DIR) + -@del $(BIN_INSTALL_DIR)\blendsh.bat + -@echo REM >> $(BIN_INSTALL_DIR)\blendsh.bat + -@echo REM This batch file starts the Jacl/TclBlend shell with a JVM >> $(BIN_INSTALL_DIR)\blendsh.bat + -@echo REM >> $(BIN_INSTALL_DIR)\blendsh.bat + -@echo set TCL_LIBRARY=$(TCL_DIR)\lib\tcl8.3>> $(BIN_INSTALL_DIR)\blendsh.bat + -@echo set PATH=$(BIN_INSTALL_DIR);$(LIB_INSTALL_DIR);$(JAVA_RUNTIME_LIBS) >> $(BIN_INSTALL_DIR)\blendsh.bat + -@echo set CLASSPATH=$(JAVA_CLASSPATH);$(LIB_INSTALL_DIR)\tclblend.jar;$(LIB_INSTALL_DIR)\tcljava.jar >> $(BIN_INSTALL_DIR)\blendsh.bat + -@echo $(JAVA) -cp %CLASSPATH% tcl.lang.Shell >> $(BIN_INSTALL_DIR)\blendsh.bat # # Check to make sure tclblend is built @@ -639,6 +669,7 @@ tcl.lang.Notifier \ tcl.lang.TclList \ tcl.lang.TimerHandler \ + tcl.lang.Shell \ tcl.lang.Util