Skip to content

Commit 9f55079

Browse files
authored
Switch back to the old Android viaduct backend (#7093)
We've been seeing a large volume of crashes on android (https://bugzilla.mozilla.org/show_bug.cgi?id=1992149). They seem to be related to the new viaduct backend. It's unclear exactly why, but let's try going back to the old one and see if it lessens the volume. This reverts commit 2042b47 so that we can use the old backend code.
1 parent d06e67e commit 9f55079

File tree

4 files changed

+501
-3
lines changed

4 files changed

+501
-3
lines changed
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.appservices.httpconfig
6+
7+
// TODO: We'd like to be using the a-c log tooling here, but adding that
8+
// dependency is slightly tricky (This also could run before its log sink
9+
// is setup!). Since logging here very much helps debugging substitution
10+
// issues, we just use logcat.
11+
12+
import android.util.Log
13+
import com.google.protobuf.CodedOutputStream
14+
import com.google.protobuf.MessageLite
15+
import com.sun.jna.DefaultTypeMapper
16+
import com.sun.jna.FromNativeContext
17+
import com.sun.jna.Library
18+
import com.sun.jna.Memory
19+
import com.sun.jna.Native
20+
import com.sun.jna.Pointer
21+
import com.sun.jna.ToNativeContext
22+
import com.sun.jna.TypeConverter
23+
import java.nio.ByteBuffer
24+
import java.nio.ByteOrder
25+
26+
/**
27+
* A helper for converting a protobuf Message into a direct `java.nio.ByteBuffer`
28+
* and its length. This avoids a copy when passing data to Rust, when compared
29+
* to using an `Array<Byte>`
30+
*/
31+
32+
fun <T : MessageLite> T.toNioDirectBuffer(): Pair<ByteBuffer, Int> {
33+
val len = this.serializedSize
34+
val nioBuf = ByteBuffer.allocateDirect(len)
35+
nioBuf.order(ByteOrder.nativeOrder())
36+
val output = CodedOutputStream.newInstance(nioBuf)
37+
this.writeTo(output)
38+
output.checkNoSpaceLeft()
39+
return Pair(first = nioBuf, second = len)
40+
}
41+
42+
sealed class MegazordError : Exception {
43+
/**
44+
* The name of the component we were trying to initialize when we had the error.
45+
*/
46+
val componentName: String
47+
48+
constructor(componentName: String, msg: String) : super(msg) {
49+
this.componentName = componentName
50+
}
51+
52+
constructor(componentName: String, msg: String, cause: Throwable) : super(msg, cause) {
53+
this.componentName = componentName
54+
}
55+
}
56+
57+
class IncompatibleMegazordVersion(
58+
componentName: String,
59+
val componentVersion: String,
60+
val megazordLibrary: String,
61+
val megazordVersion: String?,
62+
) : MegazordError(
63+
componentName,
64+
"Incompatible megazord version: library \"$componentName\" was compiled expecting " +
65+
"app-services version \"$componentVersion\", but the megazord \"$megazordLibrary\" provides " +
66+
"version \"${megazordVersion ?: "unknown"}\"",
67+
)
68+
69+
class MegazordNotInitialized(componentName: String) : MegazordError(
70+
componentName,
71+
"The application-services megazord has not yet been initialized, but is needed by \"$componentName\"",
72+
)
73+
74+
/**
75+
* I think we'd expect this to be caused by the following two things both happening
76+
*
77+
* 1. Substitution not actually replacing the full megazord
78+
* 2. Megazord initialization getting called after the first attempt to load something from the
79+
* megazord, causing us to fall back to checking the full-megazord (and finding it, because
80+
* of #1).
81+
*
82+
* It's very unlikely, but if it did happen it could be a memory safety error, so we check.
83+
*/
84+
class MultipleMegazordsPresent(
85+
componentName: String,
86+
val loadedMegazord: String,
87+
val requestedMegazord: String,
88+
) : MegazordError(
89+
componentName,
90+
"Multiple megazords are present, and bindings have already been loaded from " +
91+
"\"$loadedMegazord\" when a request to load $componentName from $requestedMegazord " +
92+
"is made. (This probably stems from an error in your build configuration)",
93+
)
94+
95+
internal const val FULL_MEGAZORD_LIBRARY: String = "megazord"
96+
97+
internal fun lookupMegazordLibrary(componentName: String, componentVersion: String): String {
98+
val mzLibrary = System.getProperty("mozilla.appservices.megazord.library")
99+
Log.d("RustNativeSupport", "lib configured: ${mzLibrary ?: "none"}")
100+
if (mzLibrary == null) {
101+
// If it's null, then the megazord hasn't been initialized.
102+
if (checkFullMegazord(componentName, componentVersion)) {
103+
return FULL_MEGAZORD_LIBRARY
104+
}
105+
Log.e(
106+
"RustNativeSupport",
107+
"megazord not initialized, and default not present. failing to init $componentName",
108+
)
109+
throw MegazordNotInitialized(componentName)
110+
}
111+
112+
// Assume it's properly initialized if it's been initialized at all
113+
val mzVersion = System.getProperty("mozilla.appservices.megazord.version")!!
114+
Log.d("RustNativeSupport", "lib version configured: $mzVersion")
115+
116+
// We require exact equality, since we don't perform a major version
117+
// bump if we change the ABI. In practice, this seems unlikely to
118+
// cause problems, but we could come up with a scheme if this proves annoying.
119+
if (componentVersion != mzVersion) {
120+
Log.e(
121+
"RustNativeSupport",
122+
"version requested by component doesn't match initialized " +
123+
"megazord version ($componentVersion != $mzVersion)",
124+
)
125+
throw IncompatibleMegazordVersion(componentName, componentVersion, mzLibrary, mzVersion)
126+
}
127+
return mzLibrary
128+
}
129+
130+
/**
131+
* Determine the megazord library name, and check that its version is
132+
* compatible with the version of our bindings. Returns the megazord
133+
* library name.
134+
*
135+
* Note: This is only public because it's called by an inline function.
136+
* It should not be called by consumers.
137+
*/
138+
@Synchronized
139+
fun findMegazordLibraryName(componentName: String, componentVersion: String): String {
140+
Log.d("RustNativeSupport", "findMegazordLibraryName($componentName, $componentVersion")
141+
val mzLibraryUsed = System.getProperty("mozilla.appservices.megazord.library.used")
142+
Log.d("RustNativeSupport", "lib in use: ${mzLibraryUsed ?: "none"}")
143+
val mzLibraryDetermined = lookupMegazordLibrary(componentName, componentVersion)
144+
Log.d("RustNativeSupport", "settled on $mzLibraryDetermined")
145+
146+
// If we've already initialized the megazord, that means we've probably already loaded bindings
147+
// from it somewhere. It would be a big problem for us to use some bindings from one lib and
148+
// some from another, so we just fail.
149+
if (mzLibraryUsed != null && mzLibraryDetermined != mzLibraryUsed) {
150+
Log.e(
151+
"RustNativeSupport",
152+
"Different than first time through ($mzLibraryDetermined != $mzLibraryUsed)!",
153+
)
154+
throw MultipleMegazordsPresent(componentName, mzLibraryUsed, mzLibraryDetermined)
155+
}
156+
157+
// Mark that we're about to load bindings from the specified lib. Note that we don't do this
158+
// in the case that the megazord check threw.
159+
if (mzLibraryUsed != null) {
160+
Log.d("RustNativeSupport", "setting first time through: $mzLibraryDetermined")
161+
System.setProperty("mozilla.appservices.megazord.library.used", mzLibraryDetermined)
162+
}
163+
return mzLibraryDetermined
164+
}
165+
166+
/**
167+
* Contains all the boilerplate for loading a library binding from the megazord,
168+
* locating it if necessary, safety-checking versions, and setting up a fallback
169+
* if loading fails.
170+
*
171+
* Indirect as in, we aren't using JNA direct mapping. Eventually we'd
172+
* like to (it's faster), but that's a problem for another day.
173+
*/
174+
inline fun <reified Lib : Library> loadIndirect(
175+
componentName: String,
176+
componentVersion: String,
177+
): Lib {
178+
val mzLibrary = findMegazordLibraryName(componentName, componentVersion)
179+
// Rust code always expects strings to be UTF-8 encoded.
180+
// Unfortunately, the `STRING_ENCODING` option doesn't seem to apply
181+
// to function arguments, only to return values, so, if the default encoding
182+
// is not UTF-8, we need to use an explicit TypeMapper to ensure that
183+
// strings are handled correctly.
184+
// Further, see also https://github.com/mozilla/uniffi-rs/issues/1044 - if
185+
// this code and our uniffi-generated code don't agree on the options, it's
186+
// possible our megazord gets loaded twice, breaking things in
187+
// creative/obscure ways.
188+
// We used to unconditionally set `options[Library.OPTION_STRING_ENCODING] = "UTF-8"`
189+
// but we now only do it if we really need to. This means in practice, both
190+
// us and uniffi agree everywhere we care about.
191+
// Assuming uniffi fixes this in the same way we've done it here, there should be
192+
// no need to adjust anything once that issue is fixed.
193+
val options: MutableMap<String, Any> = mutableMapOf()
194+
if (Native.getDefaultStringEncoding() != "UTF-8") {
195+
options[Library.OPTION_STRING_ENCODING] = "UTF-8"
196+
options[Library.OPTION_TYPE_MAPPER] = UTF8TypeMapper()
197+
}
198+
return Native.load<Lib>(mzLibrary, Lib::class.java, options)
199+
}
200+
201+
// See the comment on full_megazord_get_version for background
202+
// on why this exists and what we use it for.
203+
@Suppress("FunctionNaming", "ktlint:standard:function-naming")
204+
internal interface LibMegazordFfi : Library {
205+
// Note: Rust doesn't want us to free this string (because
206+
// it's a pain for us to arrange here), so it is actually
207+
// correct for us to return a String over the FFI for this.
208+
fun full_megazord_get_version(): String?
209+
}
210+
211+
/**
212+
* Try and load the full megazord library, call the function for getting its
213+
* version, and check it against componentVersion.
214+
*
215+
* - If the megazord does not exist, returns false
216+
* - If the megazord exists and the version is valid, returns true.
217+
* - If the megazord exists and the version is invalid, throws a IncompatibleMegazordVersion error.
218+
* (This is done here instead of returning false so that we can provide better info in the error)
219+
*/
220+
internal fun checkFullMegazord(componentName: String, componentVersion: String): Boolean {
221+
return try {
222+
Log.d(
223+
"RustNativeSupport",
224+
"No lib configured, trying full megazord",
225+
)
226+
// It's not ideal to do this every time, but it should be rare, not too costly,
227+
// and the workaround for the app is simple (just init the megazord).
228+
val lib = Native.load<LibMegazordFfi>(FULL_MEGAZORD_LIBRARY, LibMegazordFfi::class.java)
229+
230+
val version = lib.full_megazord_get_version()
231+
232+
Log.d(
233+
"RustNativeSupport",
234+
"found full megazord, it self-reports version as: ${version ?: "unknown"}",
235+
)
236+
if (version == null) {
237+
throw IncompatibleMegazordVersion(
238+
componentName,
239+
componentVersion,
240+
FULL_MEGAZORD_LIBRARY,
241+
null,
242+
)
243+
}
244+
245+
if (version != componentVersion) {
246+
Log.e(
247+
"RustNativeSupport",
248+
"found default megazord, but versions don't match ($version != $componentVersion)",
249+
)
250+
throw IncompatibleMegazordVersion(
251+
componentName,
252+
componentVersion,
253+
FULL_MEGAZORD_LIBRARY,
254+
version,
255+
)
256+
}
257+
258+
true
259+
} catch (e: UnsatisfiedLinkError) {
260+
Log.e("RustNativeSupport", "Default megazord not found: ${e.localizedMessage}")
261+
if (componentVersion.startsWith("0.0.1-SNAPSHOT")) {
262+
Log.i("RustNativeSupport", "It looks like you're using a local development build.")
263+
Log.i("RustNativeSupport", "You may need to check that `rust.targets` contains the appropriate platforms.")
264+
}
265+
false
266+
}
267+
}
268+
269+
/**
270+
* A JNA TypeMapper that always converts strings as UTF-8 bytes.
271+
*
272+
* Rust always expects strings to be in UTF-8, but JNA defaults to using the
273+
* system encoding. This is *often* UTF-8, but not always. In cases where it
274+
* isn't you can use this TypeMapper to ensure Strings are correctly
275+
* interpreted by Rust.
276+
*
277+
* The logic here is essentially the same as what JNA does by default
278+
* with String values, but explicitly using a fixed UTF-8 encoding.
279+
*
280+
*/
281+
public class UTF8TypeMapper : DefaultTypeMapper() {
282+
init {
283+
addTypeConverter(String::class.java, UTF8TypeConverter())
284+
}
285+
}
286+
287+
internal class UTF8TypeConverter : TypeConverter {
288+
override fun toNative(value: Any, context: ToNativeContext): Any {
289+
val bytes = (value as String).toByteArray(Charsets.UTF_8)
290+
val mem = Memory(bytes.size.toLong() + 1L)
291+
mem.write(0, bytes, 0, bytes.size)
292+
mem.setByte(bytes.size.toLong(), 0)
293+
return mem
294+
}
295+
296+
override fun fromNative(value: Any, context: FromNativeContext): Any {
297+
return (value as Pointer).getString(0, Charsets.UTF_8.name())
298+
}
299+
300+
override fun nativeType() = Pointer::class.java
301+
}

0 commit comments

Comments
 (0)