From 710d86d31cd278583ee3d74b36595f4148a72133 Mon Sep 17 00:00:00 2001 From: Sven Gothel Date: Sat, 5 Jun 2010 00:03:49 +0200 Subject: Minor additions to nio/Buffers, util/IntIntHashMap and os/NativeLibrary Buffers add 'float[] getFloatArray(double[])' conversion, ready to replace all JOGL InternalBufferUtil's. NativeLibrary/DynamicLinker add global lookup method allowing Unices and OSX to lookup a symbol globally. However, this is not recommended, due to the lookup costs. Windows is not supported here. Primitive type HashMap's (IntIntHashMap): Added putAll() --- src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java') diff --git a/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java b/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java index 2858f74..935f386 100755 --- a/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java +++ b/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java @@ -65,6 +65,10 @@ public class WindowsDynamicLinkerImpl implements DynamicLinker { return addr; } + public long lookupSymbolGlobal(String symbolName) { + throw new RuntimeException("lookupSymbolGlobal: Not supported on Windows"); + } + public void closeLibrary(long libraryHandle) { FreeLibrary(libraryHandle); } -- cgit v1.2.3 From 555091d37a9a44fb7c35479ddecfee358a559e90 Mon Sep 17 00:00:00 2001 From: Sven Gothel Date: Thu, 10 Jun 2010 06:13:06 +0200 Subject: Adding DynamicLibraryBundle utility to bundle Tool and JNI native library loading and lookup Add JNILibLoaderBase.loadLibrary(String libname, boolean ignoreError); DynamicLibraryBundle provides Tool and JNI native library loading and lookup New classes: com.jogamp.common.os.DynamicLibraryBundle com.jogamp.common.os.DynamicLibraryBundleInfo com.jogamp.common.util.MiscUtils.java Change: DEBUG/VERBOSE properties 'gluegen' -> 'jogamp' --- .../com/jogamp/common/jvm/JNILibLoaderBase.java | 89 +++--- .../com/jogamp/common/os/DynamicLibraryBundle.java | 327 +++++++++++++++++++++ .../jogamp/common/os/DynamicLibraryBundleInfo.java | 75 +++++ src/java/com/jogamp/common/os/NativeLibrary.java | 6 +- .../jogamp/common/os/WindowsDynamicLinkerImpl.java | 2 +- src/java/com/jogamp/common/util/MiscUtils.java | 57 ++++ .../jogamp/gluegen/runtime/ProcAddressTable.java | 7 +- 7 files changed, 522 insertions(+), 41 deletions(-) create mode 100755 src/java/com/jogamp/common/os/DynamicLibraryBundle.java create mode 100644 src/java/com/jogamp/common/os/DynamicLibraryBundleInfo.java create mode 100644 src/java/com/jogamp/common/util/MiscUtils.java (limited to 'src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java') diff --git a/src/java/com/jogamp/common/jvm/JNILibLoaderBase.java b/src/java/com/jogamp/common/jvm/JNILibLoaderBase.java index be5b1de..8d26bf4 100644 --- a/src/java/com/jogamp/common/jvm/JNILibLoaderBase.java +++ b/src/java/com/jogamp/common/jvm/JNILibLoaderBase.java @@ -45,7 +45,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessController; import java.security.AccessControlContext; -import java.security.PrivilegedAction; import java.util.HashSet; import com.jogamp.common.impl.Debug; @@ -55,43 +54,56 @@ public class JNILibLoaderBase { public interface LoaderAction { /** - * Loads the library specified by libname. Optionally preloads the libraries specified by - * preload. The implementation should ignore, if the preload-libraries have already been - * loaded. + * Loads the library specified by libname.
+ * The implementation should ignore, if the library has been loaded already.
+ * @param libname the library to load + * @param ignoreError if true, errors during loading the library should be ignored + * @return true if library loaded successful + */ + boolean loadLibrary(String libname, boolean ignoreError); + + /** + * Loads the library specified by libname.
+ * Optionally preloads the libraries specified by preload.
+ * The implementation should ignore, if any library has been loaded already.
* @param libname the library to load * @param preload the libraries to load before loading the main library if not null - * @param preloadIgnoreError true, if errors during loading the preload-libraries should be ignored + * @param preloadIgnoreError if true, errors during loading the preload-libraries should be ignored */ - void loadLibrary(String libname, String[] preload, - boolean preloadIgnoreError); + void loadLibrary(String libname, String[] preload, boolean preloadIgnoreError); } private static class DefaultAction implements LoaderAction { - public void loadLibrary(String libname, String[] preload, boolean preloadIgnoreError) { - if (null!=preload) { - for (int i=0; i + *
    + *
  • The to-be-glued native library, eg OpenGL32.dll. From hereon this is referred as the Tool.
  • + *
  • The JNI glue-code native library, eg jogl_desktop.dll. From heron this is referred as the Glue
  • + *

+ * An instance provides a complete {@link com.jogamp.common.os.DynamicLookupHelper} + * to {@link com.jogamp.gluegen.runtime.ProcAddressTable#reset(com.jogamp.common.os.DynamicLookupHelper lookup)} + * the {@link com.jogamp.gluegen.runtime.ProcAddressTable}.
+ * At construction, it loads the Tool native library, loads Glue native library via + * {@link com.jogamp.common.os.DynamicLibraryBundleInfo#loadJNILibrary()} + * and resolves the optional Tool's
GetProcAddress
using {@link com.jogamp.common.os.DynamicLibraryBundleInfo#getToolGetProcAddressFuncNameList()}. + */ +public class DynamicLibraryBundle implements DynamicLookupHelper { + protected static final boolean DEBUG = NativeLibrary.DEBUG; + protected static final boolean DEBUG_LOOKUP = NativeLibrary.DEBUG_LOOKUP; + + private DynamicLibraryBundleInfo info; + + private List/*>*/ toolLibNames; + private boolean[] toolLibLoaded; + private int toolLibLoadedNumber; + protected List/**/ nativeLibraries; + + private List/**/ glueLibNames; + private boolean[] glueLibLoaded; + private int glueLibLoadedNumber; + + private long toolGetProcAddressHandle; + private HashSet toolGetProcAddressFuncNameSet; + private List toolGetProcAddressFuncNameList; + + public DynamicLibraryBundle(DynamicLibraryBundleInfo info) { + if(null==info) { + throw new RuntimeException("Null DynamicLibraryBundleInfo"); + } + this.info = info; + if(DEBUG) { + System.out.println("DynamicLibraryBundle.init start with: "+info.getClass().getName()); + } + nativeLibraries = new ArrayList(); + toolLibNames = info.getToolLibNames(); + glueLibNames = info.getGlueLibNames(); + loadLibraries(); + toolGetProcAddressFuncNameList = info.getToolGetProcAddressFuncNameList(); + if(null!=toolGetProcAddressFuncNameList) { + toolGetProcAddressFuncNameSet = new HashSet(toolGetProcAddressFuncNameList); + toolGetProcAddressHandle = getToolGetProcAddressHandle(); + } else { + toolGetProcAddressFuncNameSet = new HashSet(); + toolGetProcAddressHandle = 0; + } + if(DEBUG) { + System.out.println("DynamicLibraryBundle.init Summary: "+info.getClass().getName()); + System.out.println(" toolGetProcAddressFuncNameList: "+MiscUtils.toString(toolGetProcAddressFuncNameList)); + System.out.println(" Tool Lib Names : "+MiscUtils.toString(toolLibNames)); + System.out.println(" Tool Lib Loaded: "+getToolLibLoadedNumber()+"/"+getToolLibNumber()+", complete "+isToolLibComplete()); + System.out.println(" Glue Lib Names : "+MiscUtils.toString(glueLibNames)); + System.out.println(" Glue Lib Loaded: "+getGlueLibLoadedNumber()+"/"+getGlueLibNumber()+", complete "+isGlueLibComplete()); + System.out.println(" All Complete: "+isLibComplete()); + } + } + + public final boolean isLibComplete() { + return isToolLibComplete() && isGlueLibComplete() ; + } + + public final int getToolLibNumber() { + return toolLibNames.size(); + } + + public final int getToolLibLoadedNumber() { + return toolLibLoadedNumber; + } + + public final boolean isToolLibComplete() { + return getToolLibNumber() == getToolLibLoadedNumber(); + } + + public final boolean isToolLibLoaded() { + return 0 < toolLibLoadedNumber; + } + + public final boolean isToolLibLoaded(int i) { + if(0 <= i && i < toolLibLoaded.length) { + return toolLibLoaded[i]; + } + return false; + } + + public final int getGlueLibNumber() { + return glueLibNames.size(); + } + + public final int getGlueLibLoadedNumber() { + return glueLibLoadedNumber; + } + + public final boolean isGlueLibComplete() { + return getGlueLibNumber() == getGlueLibLoadedNumber(); + } + + public final boolean isGlueLibLoaded(int i) { + if(0 <= i && i < glueLibLoaded.length) { + return glueLibLoaded[i]; + } + return false; + } + + public final DynamicLibraryBundleInfo getBundleInfo() { return info; } + + protected long getToolGetProcAddressHandle() { + if(!isToolLibLoaded()) { + return 0; + } + long aptr = 0; + for(Iterator iter=toolGetProcAddressFuncNameList.iterator(); 0==aptr && iter.hasNext(); ) { + String name = (String) iter.next(); + aptr = dynamicLookupFunctionOnLibs(name); + if(DEBUG) { + System.out.println("getToolGetProcAddressHandle: "+name+" -> 0x"+Long.toHexString(aptr)); + } + } + return aptr; + } + + protected NativeLibrary loadFirstAvailable(List/**/ libNames, ClassLoader loader, boolean global) { + for (Iterator iter = libNames.iterator(); iter.hasNext(); ) { + NativeLibrary lib = NativeLibrary.open((String) iter.next(), loader, global); + if (lib != null) { + return lib; + } + } + return null; + } + + private void loadLibraries() { + if( null == toolLibNames || toolLibNames.size() == 0) { + if(DEBUG) { + System.out.println("No Tool native library names given"); + } + return; + } + + if( null == glueLibNames || glueLibNames.size() == 0 ) { + if(DEBUG) { + System.out.println("No Glue native library names given"); + } + return; + } + + toolLibLoadedNumber = 0; + int i; + toolLibLoaded = new boolean[toolLibNames.size()]; + for(i=0; i*/ libNames = null; + if(listObj instanceof List) { + libNames = (List) listObj; + } else if(listObj instanceof String) { + libNames = new ArrayList(); + libNames.add((String)listObj); + } else { + throw new RuntimeException("List element "+i+" must be either a List or String: "+MiscUtils.toString(toolLibNames)); + } + if( null != libNames && libNames.size() > 0 ) { + lib = loadFirstAvailable(libNames, loader, info.shallLinkGlobal()); + if ( null == lib ) { + if(DEBUG) { + System.out.println("Unable to load any Tool library of: "+MiscUtils.toString(libNames)); + } + } else { + nativeLibraries.add(lib); + toolLibLoaded[i]=true; + toolLibLoadedNumber++; + if(DEBUG) { + System.out.println("Loaded Tool library: "+lib); + } + } + } + } + if( !isToolLibLoaded() ) { + if(DEBUG) { + System.out.println("No Tool libraries loaded"); + } + return; + } + + glueLibLoadedNumber = 0; + i=0; + for(Iterator iter = glueLibNames.iterator(); iter.hasNext(); i++) { + String libName = (String) iter.next(); + boolean ignoreError = true; + boolean res; + try { + res = GlueJNILibLoaderBase.loadLibrary(libName, ignoreError); + if(DEBUG && !res) { + System.out.println("Info: Could not load JNI/Glue library: "+libName); + } + } catch (UnsatisfiedLinkError e) { + res = false; + if(DEBUG) { + System.out.println("Unable to load JNI/Glue library: "+libName); + e.printStackTrace(); + } + } + glueLibLoaded[i] = res; + if(res) { + glueLibLoadedNumber++; + } + } + } + + private long dynamicLookupFunctionOnLibs(String funcName) { + if(!isToolLibLoaded() || null==funcName) { + if(DEBUG_LOOKUP && !isToolLibLoaded()) { + System.err.println("Lookup-Native: <" + funcName + "> ** FAILED ** Tool native library not loaded"); + } + return 0; + } + long addr = 0; + NativeLibrary lib = null; + + if(info.shallLookupGlobal()) { + // Try a global symbol lookup first .. + addr = NativeLibrary.dynamicLookupFunctionGlobal(funcName); + } + // Look up this function name in all known libraries + for (Iterator iter = nativeLibraries.iterator(); 0==addr && iter.hasNext(); ) { + lib = (NativeLibrary) iter.next(); + addr = lib.dynamicLookupFunction(funcName); + } + if(DEBUG_LOOKUP) { + String libName = ( null == lib ) ? "GLOBAL" : lib.toString(); + if(0!=addr) { + System.err.println("Lookup-Native: <" + funcName + "> 0x" + Long.toHexString(addr) + " in lib " + libName ); + } else { + System.err.println("Lookup-Native: <" + funcName + "> ** FAILED ** in libs " + MiscUtils.toString(nativeLibraries)); + } + } + return addr; + } + + public long dynamicLookupFunction(String funcName) { + if(!isToolLibLoaded() || null==funcName) { + if(DEBUG_LOOKUP && !isToolLibLoaded()) { + System.err.println("Lookup: <" + funcName + "> ** FAILED ** Tool native library not loaded"); + } + return 0; + } + + if(toolGetProcAddressFuncNameSet.contains(funcName)) { + return toolGetProcAddressHandle; + } + + long addr = 0; + + if(0 != toolGetProcAddressHandle) { + addr = info.toolDynamicLookupFunction(toolGetProcAddressHandle, funcName); + if(DEBUG_LOOKUP) { + if(0!=addr) { + System.err.println("Lookup-Tool: <"+funcName+"> 0x"+Long.toHexString(addr)); + } + } + } + if(0==addr) { + addr = dynamicLookupFunctionOnLibs(funcName); + } + return addr; + } + + /** Inherit access */ + static class GlueJNILibLoaderBase extends JNILibLoaderBase { + protected static synchronized boolean loadLibrary(String libname, boolean ignoreError) { + return JNILibLoaderBase.loadLibrary(libname, ignoreError); + } + } +} + diff --git a/src/java/com/jogamp/common/os/DynamicLibraryBundleInfo.java b/src/java/com/jogamp/common/os/DynamicLibraryBundleInfo.java new file mode 100644 index 0000000..37ca538 --- /dev/null +++ b/src/java/com/jogamp/common/os/DynamicLibraryBundleInfo.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2010, Sven Gothel + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Sven Gothel nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL Sven Gothel BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jogamp.common.os; + +import java.util.*; +import java.security.*; + +public interface DynamicLibraryBundleInfo { + public static final boolean DEBUG = DynamicLibraryBundle.DEBUG; + + /** @return a list of Tool library names or alternative library name lists.
+ *
    + *
  • GL/GLU example Unix: [ [ "libGL.so.1", "libGL.so", "GL" ], [ "libGLU.so", "GLU" ] ]
  • + *
  • GL/GLU example Windows: [ "OpenGL32", "GLU32" ]
  • + *
  • Cg/CgGL example: [ [ "libCg.so", "Cg" ], [ "libCgGL.so", "CgGL" ] ]
  • + * + */ + public List getToolLibNames(); + + /** @return a list of Glue library names.
    + *
      + *
    • GL: [ "nativewindow_x11", "jogl_gl2es12", "jogl_desktop" ]
    • + *
    • NEWT: [ "nativewindow_x11", "newt" ]
    • + *
    • Cg: [ "nativewindow_x11", "jogl_cg" ]
    • + *

    + * Only the last entry is crucial, ie all other are optional preload dependencies and may generate errors, + * which are ignored. + */ + public List/**/ getGlueLibNames(); + + /** May return the native libraries
    GetProcAddressFunc
    names, the first found function is being used.
    + * This could be eg:
     glXGetProcAddressARB, glXGetProcAddressARB 
    .
    + * If your Tool does not has this facility, just return null. + */ + public List getToolGetProcAddressFuncNameList() ; + + /** May implement the lookup function using the Tools facility.
    + * The actual function pointer is provided to allow proper bootstrapping of the ProcAddressTable.
    + */ + public long toolDynamicLookupFunction(long toolGetProcAddressHandle, String funcName); + + /** @return true if the native library symbols shall be made available for symbol resolution of subsequently loaded libraries. */ + public boolean shallLinkGlobal(); + + /** @return true if the dynamic symbol lookup shall happen system wide, over all loaded libraries. Otherwise only the loaded native libraries are used for lookup, which shall be the default. */ + public boolean shallLookupGlobal(); + +} + + diff --git a/src/java/com/jogamp/common/os/NativeLibrary.java b/src/java/com/jogamp/common/os/NativeLibrary.java index 897e240..97aabae 100755 --- a/src/java/com/jogamp/common/os/NativeLibrary.java +++ b/src/java/com/jogamp/common/os/NativeLibrary.java @@ -62,7 +62,8 @@ public class NativeLibrary implements DynamicLookupHelper { private static final int WINDOWS = 1; private static final int UNIX = 2; private static final int MACOSX = 3; - private static boolean DEBUG; + protected static boolean DEBUG; + protected static boolean DEBUG_LOOKUP; private static int platform; private static DynamicLinker dynLink; private static String[] prefixes; @@ -81,7 +82,8 @@ public class NativeLibrary implements DynamicLookupHelper { platform = UNIX; } - DEBUG = (System.getProperty("gluegen.debug.NativeLibrary") != null); + DEBUG = (System.getProperty("jogamp.debug.NativeLibrary") != null); + DEBUG_LOOKUP = (System.getProperty("jogamp.debug.NativeLibrary.Lookup") != null); return null; } diff --git a/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java b/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java index 935f386..7bbfe23 100755 --- a/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java +++ b/src/java/com/jogamp/common/os/WindowsDynamicLinkerImpl.java @@ -11,7 +11,7 @@ public class WindowsDynamicLinkerImpl implements DynamicLinker { static { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { - DEBUG = (System.getProperty("gluegen.debug.NativeLibrary") != null); + DEBUG = (System.getProperty("jogamp.debug.NativeLibrary") != null); return null; } }); diff --git a/src/java/com/jogamp/common/util/MiscUtils.java b/src/java/com/jogamp/common/util/MiscUtils.java new file mode 100644 index 0000000..dd32951 --- /dev/null +++ b/src/java/com/jogamp/common/util/MiscUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010, Sven Gothel + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Sven Gothel nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL Sven Gothel BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.jogamp.common.util; + +import java.util.*; + +public class MiscUtils { + + public static StringBuffer append(StringBuffer sb, List list) { + sb.append("["); + if(null!=list&&list.size()>0) { + boolean comma = false; + for(Iterator iter=list.iterator(); iter.hasNext(); ) { + if(comma) { sb.append(", "); } + Object o = iter.next(); + if(o instanceof List) { + sb = append(sb, (List)o); + } else { + sb.append(o); + } + comma = true; + } + } + sb.append("]"); + return sb; + } + + public static String toString(List list) { + return append(new StringBuffer(), list).toString(); + } +} + diff --git a/src/java/com/jogamp/gluegen/runtime/ProcAddressTable.java b/src/java/com/jogamp/gluegen/runtime/ProcAddressTable.java index 9a05fce..f0be20f 100644 --- a/src/java/com/jogamp/gluegen/runtime/ProcAddressTable.java +++ b/src/java/com/jogamp/gluegen/runtime/ProcAddressTable.java @@ -65,9 +65,9 @@ public abstract class ProcAddressTable { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { - DEBUG = (System.getProperty("gluegen.debug.ProcAddressHelper") != null); + DEBUG = (System.getProperty("jogamp.debug.ProcAddressHelper") != null); if (DEBUG) { - DEBUG_PREFIX = System.getProperty("gluegen.debug.ProcAddressHelper.prefix"); + DEBUG_PREFIX = System.getProperty("jogamp.debug.ProcAddressHelper.prefix"); } return null; } @@ -90,6 +90,9 @@ public abstract class ProcAddressTable { public void reset(DynamicLookupHelper lookup) throws RuntimeException { + if(null==lookup) { + throw new RuntimeException("Passed null DynamicLookupHelper"); + } Class tableClass = getClass(); Field[] fields = tableClass.getFields(); -- cgit v1.2.3