Skip to content

Commit f234612

Browse files
[native] Use full marshal method signature when a method is overloaded (#10433)
Fixes: #10417 Context: https://docs.oracle.com/en/java/javase/17/docs/specs/jni/design.html#resolving-native-method-names Context: #10417 (comment) Whenever we generate marshal methods code for overloaded Java methods, we must be sure to use full native signature instead of the short form, even if one of the overloads doesn't have parameters. Failure to do so results in hard-to fix subtle bugs like in case of #10417. In this instance, we had a class (`Android.Runtime.InputStreamAdapter`) with 3 method overloads: * `public override int Read ();` * `public override int Read (byte[] bytes)` * `public override int Read (byte[] bytes, int offset, int length)` The sample application failed to load an image using a Glide decoder, which called the `Read (byte[], int, int)` overload above. It was puzzling, because the Java code clearly called the right native function and yet, it ended up in a marshal method wrapper for the `Read ()` overload. The cause became apparent when I checked names of the generated native functions. All of them were valid and yet the whole was subtly broken: the `Read ()` overload used a (valid) "short form" native symbol name, which omits the part that encodes parameter types in the symbol name. The reason for this was that the generator code saw there were no parameters and so it output the short form, because it is the **first** symbol name looked up by JavaVM - thus it seemed to be a good optimization at the time of implementing the marshal methods generator. The problems began when **both** overloads were used somewhere in the application code and the `Read()` overload was **always** found first, since it used the short form name, and thus Java never got to looking up the other overload! Then it proceeded to call the parameter-less overload, which caused sort-of worked (as in it didn't crash) it it was far from correct. The fix implemented here is to always emit the `__` native symbol suffix for any overloaded methods that have no parameters. * Added on-device test to repro issue. These tests should return 1024, but fail with `$(AndroidEnableMarshalMethods)`: 08-21 11:53:13.738 16263 16289 I NUnit : InputStreamAdapter_Read_bytes 08-21 11:53:13.738 16263 16289 D StreamTest: StreamTest.InputStreamAdapter_Read_bytes, underlying stream type: mono.android.runtime.InputStreamAdapter 08-21 11:53:13.745 16263 16289 E NUnit : [FAIL] 08-21 11:53:13.746 16263 16289 E NUnit : : Expected: 1024 08-21 11:53:13.746 16263 16289 E NUnit : But was: 0 08-21 11:53:13.746 16263 16289 E NUnit : at System.IOTests.StreamTest.InputStreamAdapter_Read_bytes() If `0` is returned, it means that `read()` was called instead of the `read(...)` overload which reproduces the issue. This test appears to pass with the above changes. Co-authored-by: Jonathan Peppers <[email protected]>
1 parent 988371e commit f234612

File tree

4 files changed

+73
-2
lines changed

4 files changed

+73
-2
lines changed

src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,9 +346,14 @@ string MakeNativeSymbolName (MarshalMethodEntryObject entry, bool useFullNativeS
346346
}
347347

348348
string sigParams = signature.Substring (1, sigEndIdx - 1);
349-
if (sigParams.Length > 0) {
349+
bool haveParams = sigParams.Length > 0;
350+
if (useFullNativeSignature || haveParams) {
351+
// We always append the underscores for overloaded methods, see https://github.com/dotnet/android/issues/10417#issuecomment-3210789627
352+
// for the reason why
350353
sb.Append ("__");
351-
sb.Append (MangleForJni (sigParams));
354+
if (haveParams) {
355+
sb.Append (MangleForJni (sigParams));
356+
}
352357
}
353358
}
354359

tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
<Compile Include="System.Drawing\TypeConverterTest.cs" />
106106
<Compile Include="System.IO\DirectoryTest.cs" />
107107
<Compile Include="System.IO\DriveInfoTest.cs" />
108+
<Compile Include="System.IO\StreamTest.cs" />
108109
<Compile Include="System.IO.Compression\GZipStreamTest.cs" />
109110
<Compile Include="System.Linq\LinqExpressionTest.cs" />
110111
<Compile Include="System.Net\ProxyTest.cs" />
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.IO;
3+
using NUnit.Framework;
4+
using JavaStreamTest = global::Net.Dot.Android.Test.StreamTest;
5+
6+
namespace System.IOTests;
7+
8+
[TestFixture]
9+
public class StreamTest
10+
{
11+
[Test]
12+
public void InputStreamAdapter_Read ()
13+
{
14+
using var stream = new MemoryStream (new byte [JavaStreamTest.BufferSize]);
15+
var result = JavaStreamTest.InputStreamAdapter_Read (stream);
16+
Assert.AreEqual (0, result); // First byte is 0
17+
}
18+
19+
[Test]
20+
public void InputStreamAdapter_Read_bytes ()
21+
{
22+
using var stream = new MemoryStream (new byte [JavaStreamTest.BufferSize]);
23+
var result = JavaStreamTest.InputStreamAdapter_Read_bytes (stream);
24+
Assert.AreEqual (JavaStreamTest.BufferSize, result);
25+
}
26+
27+
[Test]
28+
public void InputStreamAdapter_Read_bytes_int_int ()
29+
{
30+
using var stream = new MemoryStream (new byte [JavaStreamTest.BufferSize]);
31+
var result = JavaStreamTest.InputStreamAdapter_Read_bytes_int_int (stream);
32+
Assert.AreEqual (JavaStreamTest.BufferSize, result);
33+
}
34+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.dot.android.test;
2+
3+
import android.util.Log;
4+
import java.io.InputStream;
5+
import java.io.IOException;
6+
7+
public class StreamTest
8+
{
9+
private static final String TAG = "StreamTest";
10+
public static final int BUFFER_SIZE = 1024;
11+
12+
public static int InputStreamAdapter_Read(InputStream stream) throws IOException
13+
{
14+
Log.d(TAG, "StreamTest.InputStreamAdapter_Read, underlying stream type: " + stream.getClass().getName());
15+
return stream.read();
16+
}
17+
18+
public static int InputStreamAdapter_Read_bytes(InputStream stream) throws IOException
19+
{
20+
Log.d(TAG, "StreamTest.InputStreamAdapter_Read_bytes, underlying stream type: " + stream.getClass().getName());
21+
byte[] buffer = new byte[BUFFER_SIZE];
22+
return stream.read(buffer);
23+
}
24+
25+
public static int InputStreamAdapter_Read_bytes_int_int(InputStream stream) throws IOException
26+
{
27+
Log.d(TAG, "StreamTest.InputStreamAdapter_Read_bytes_int_int, underlying stream type: " + stream.getClass().getName());
28+
byte[] buffer = new byte[BUFFER_SIZE];
29+
return stream.read(buffer, 0, BUFFER_SIZE);
30+
}
31+
}

0 commit comments

Comments
 (0)