Work around Java's insane lack of finalization guarantees
authorMatt Corallo <git@bluematt.me>
Tue, 14 Dec 2021 22:38:49 +0000 (22:38 +0000)
committerMatt Corallo <git@bluematt.me>
Tue, 21 Dec 2021 01:07:18 +0000 (01:07 +0000)
java_strings.py
pom.xml
src/main/java/org/ldk/batteries/NioPeerHandler.java

index 95c65ce7bc9aa65ed58c135abb5c5bdc8b2355fe..3a72a5b8d83a27d4579c7f658df149ccfed293d3 100644 (file)
@@ -515,6 +515,7 @@ import org.ldk.impl.bindings;
 import org.ldk.enums.*;
 import org.ldk.util.*;
 import java.util.Arrays;
+import java.lang.ref.Reference;
 import javax.annotation.Nullable;
 
 """
@@ -1307,6 +1308,45 @@ import javax.annotation.Nullable;
                 else:
                     out_java_struct += (info.arg_name)
             out_java_struct += (");\n")
+
+            # This is completely nuts. The OpenJDK JRE JIT will optimize out a object which is on
+            # the stack, calling its finalizer immediately even if member methods are *actively
+            # executing* on the same object, as long as said object is on the stack. There is no
+            # concrete specification for when the optimizer is allowed to do this, and when it is
+            # not, so there is absolutely no way to be certain that this fix suffices.
+            #
+            # Instead, the "Java Language Specification" says only that an object is reachable
+            # (i.e. will not yet be finalized) if it "can be accessed in any potential continuing
+            # computation from any live thread". To any sensible reader this would mean actively
+            # executing a member function on an object would make it not eligible for finalization.
+            # But, no, dear reader, this statement does not say that. Well, okay, it says that,
+            # very explicitly in fact, but those are just, like, words, man.
+            #
+            # In the seemingly non-normative text further down, a few examples of things the
+            # optimizer can do are given, including "if the values in an object's fields are
+            # stored in registers[, t]he may then access the registers instead of the object, and
+            # never access the object again[, implying] that the object is garbage". This appears
+            # to fully contradict both the above statement, the API documentation in java.lang.ref
+            # regarding when a reference is "strongly reachable", and basic common sense. There is
+            # no concrete set of limitations stated, however, seemingly implying the JIT could
+            # decide your code would run faster by simply garbage collecting everything
+            # immediately, ensuring your code finishes soon, just by SEGFAULT. Thus, we're really
+            # entirely flying blind here. We add some fences and hope that its sufficient, but
+            # with no specification to rely on, we cannot be certain of anything.
+            #
+            # TL;DR: The Java Language "Specification" provides no real guarantees on when an
+            # object will be considered available for garbage collection once the JIT kicks in, so
+            # we put in some fences and hope to god the JIT doesn't get smarter/more broken.
+            for idx, info in enumerate(argument_types):
+                if idx == 0 and takes_self:
+                    out_java_struct += ("\t\tReference.reachabilityFence(this);\n")
+                elif info.arg_name in default_constructor_args:
+                    for explode_idx, explode_arg in enumerate(default_constructor_args[info.arg_name]):
+                        expl_arg_name = info.arg_name + "_" + explode_arg.arg_name
+                        out_java_struct += ("\t\tReference.reachabilityFence(" + expl_arg_name + ");\n")
+                elif info.c_ty != "void":
+                    out_java_struct += ("\t\tReference.reachabilityFence(" + info.arg_name + ");\n")
+
             if return_type_info.java_ty == "long" and return_type_info.java_hu_ty != "long":
                 out_java_struct += "\t\tif (ret >= 0 && ret <= 4096) { return null; }\n"
 
diff --git a/pom.xml b/pom.xml
index 5f5342d340503377b403fffaf8bab358635ac035..b8ea0bcf01a6c39640724a3ca143b0701c5337ee 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -63,8 +63,8 @@
         </dependency>
     </dependencies>
     <properties>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
+        <maven.compiler.source>9</maven.compiler.source>
+        <maven.compiler.target>9</maven.compiler.target>
         <org.lightningdevkit.skipdocs>true</org.lightningdevkit.skipdocs>
     </properties>
 
index 49f60c7f721b22f570dd5e5d9c300ca9c9503fbe..53a5353c2738f5ac4f0f0b1e9e471269b2e50196 100644 (file)
@@ -5,6 +5,7 @@ import org.ldk.structs.*;
 
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.lang.ref.Reference;
 import java.util.LinkedList;
 import java.net.SocketAddress;
 import java.net.StandardSocketOptions;
@@ -339,6 +340,7 @@ public class NioPeerHandler {
                 }
             } catch (IOException ignored) {}
         }
+        Reference.reachabilityFence(this.peer_manager); // Almost certainly overkill, but no harm in it
     }
 
     /**