Repository: jpype-project/jpype (language: python)

Issue:
convertStrings=True cut Java returned Strings with NULL bytes short
#  Summary

When `jpype.startJVM(convertStrings=True)` is used, returned Strings with NULL bytes are cut short.
This behavior is not observed when `jpype.startJVM(convertStrings=False)` is used.

## Example Reproducer

Code is available at: https://github.com/Christopher-Chianelli/issue-reproducer/tree/jpype-null-bytes-in-string

Java class:
```java
package org.acme;

public class MyClass {
    public static int getLength(String argument) {
        return argument.length();
    }

    public static String getArgument(String argument) {
        return argument;
    }
}
```

Python code
```python
import jpype
import jpype.imports

jpype.startJVM(classpath=[...], convertStrings=True)

from org.acme import MyClass

# Passing a string with NULL bytes to Java works
assert MyClass.getLength('\u0000') == 1

# Reading a returned string with NULL bytes from Java fails
out = MyClass.getArgument('String\u0000with\u0000NULL\u0000Bytes')
print(out)

assert out == 'String\u0000with\u0000NULL\u0000Bytes'
```

### Expected Behavior

`out == 'String\u0000with\u0000NULL\u0000Bytes'`

### Actual Behavior

`out == 'String'`


Discussion / hints:
I think this is caused by this line:

https://github.com/jpype-project/jpype/blob/82a7658277b36c628ea305ea705cd012d81471dd/native/common/jp_stringtype.cpp#L50

which cause a string with null bytes to be terminated early. `PyUnicode_FromStringAndSize` (https://docs.python.org/3/c-api/unicode.html#c.PyUnicode_FromStringAndSize ) might fix the issue.

Fix patch:
diff --git a/native/common/jp_stringtype.cpp b/native/common/jp_stringtype.cpp
index 4ba607d6f..848c8a572 100644
--- a/native/common/jp_stringtype.cpp
+++ b/native/common/jp_stringtype.cpp
@@ -47,7 +47,7 @@ JPPyObject JPStringType::convertToPythonObject(JPJavaFrame& frame, jvalue val, b
 		if (context->getConvertStrings())
 		{
 			string str = frame.toStringUTF8((jstring) (val.l));
-			return JPPyObject::call(PyUnicode_FromString(str.c_str()));
+			return JPPyObject::call(PyUnicode_FromStringAndSize(str.c_str(), str.length()));
 		}
 	}
 


Test patch:
diff --git a/test/harness/jpype/str/Test.java b/test/harness/jpype/str/Test.java
index 3552501d1..5803b15f7 100644
--- a/test/harness/jpype/str/Test.java
+++ b/test/harness/jpype/str/Test.java
@@ -31,6 +31,14 @@ public String memberCall()
     return "memberCall";
   }
 
+  public static String callWithNullBytes() {
+    return "call\0With\0Null\0Bytes";
+  }
+
+  public static String returnArgument(String argument) {
+    return argument;
+  }
+
   public static final String array[] =
   {
     "apples", "banana", "cherries", "dates", "elderberry"
diff --git a/test/jpypetest/test_legacy.py b/test/jpypetest/test_legacy.py
index 4698d6e85..e0708d0b3 100644
--- a/test/jpypetest/test_legacy.py
+++ b/test/jpypetest/test_legacy.py
@@ -79,3 +79,18 @@ def testProxy(self):
         r = self._test().callProxy(p, "roundtrip")
         self.assertEqual(r, "roundtrip")
         self.assertIsInstance(r, str)
+
+    def testNullBytes(self):
+        s = self._test.callWithNullBytes()
+        self.assertEqual(s, "call\u0000With\u0000Null\u0000Bytes")
+        self.assertIsInstance(s, str)
+
+        s = self._test.returnArgument("\u0394 16 byte encoded text\u0000with null\u0000 bytes \u1394")
+        self.assertEqual(s, "\u0394 16 byte encoded text\u0000with null\u0000 bytes \u1394")
+        self.assertIsInstance(s, str)
+
+        s = self._test.returnArgument("\U0001F468\u200D\U0001F9B2 32 byte encoded text"
+                                      "\u0000with null\u0000 bytes \U0001F468\u200D\U0001F9B2")
+        self.assertEqual(s, "\U0001F468\u200D\U0001F9B2 32 byte encoded text"
+                            "\u0000with null\u0000 bytes \U0001F468\u200D\U0001F9B2")
+        self.assertIsInstance(s, str)