Лечим SQLite в Monotouch или практическая польза рефлексии

Работа с детищем Xamarin интересна и полна сюрпризов, как в хорошем смысле слова, так и в плохом. Одни проблемы решаются при помощи гугла и StackOverflow, другие же требуют нестандартного подхода. В данной статье я хочу рассказать историю о том, как можно с помощью исходников, рефлексии и трех кружек чая решить одну пренеприятнейшую проблему.А проблема заключается в том, что Monotouch не поддерживает пользовательские функции в SQLite. Попытка подключить их через стандартный API приводит к ошибке вида:

Attempting to JIT compile method '(wrapper native-to-managed) Mono.Data.Sqlite.SqliteFunction: ScalarCallback (intptr, int, intptr)' while running with --aot-only. Seehttp://docs.xamarin.com/ios/about/limitations for more information.

А это значит, что необходимо лезть в исходники mono, что мы сейчас и сделаем: https://github.com/mono/mono. Код SQLite расположен по пути: \mono\mcs\class\Mono.Data.Sqlite\Mono.Data.Sqlite_2.0.Поиск причины Для начала прочитаем статью ограничения платформы monotouch. Так как мы передаем функцию обратного вызова, то начинают играть роль Reverce Callbacks ограничения: Метод должен иметь атрибут MonoPInvokeCallbackAttribute Он должен быть статическими Далее найдем код обращения к нативному API SQLite, а именно класс UnsafeNativeMethods. В руководстве по SQLite написано, что для подключения функций необходимо вызвать метод sqlite3_create_function. Само собой, он вызывается через DllImport. Очевидно, что в качестве одного из параметров передается наша функция обратного вызова.Сам метод sqlite3_create_function вызывается из SQLite3.CreateFunction (): Метод CreateFunction internal override void CreateFunction (string strFunction, int nArgs, bool needCollSeq, SQLiteCallback func, SQLiteCallback funcstep, SQLiteFinalCallback funcfinal) { int n = UnsafeNativeMethods.sqlite3_create_function (_sql, ToUTF8(strFunction), nArgs, 4, IntPtr.Zero, func, funcstep, funcfinal); if (n == 0) n = UnsafeNativeMethods.sqlite3_create_function (_sql, ToUTF8(strFunction), nArgs, 1, IntPtr.Zero, func, funcstep, funcfinal); if (n > 0) throw new SqliteException (n, SQLiteLastError ()); } Который в свою очередь используется в SQLiteFunction.BindFunctions: Метод BindFunctions internal static SqliteFunction[] BindFunctions (SQLiteBase sqlbase) { SqliteFunction f; List lFunctions = new List(); foreach (SqliteFunctionAttribute pr in _registeredFunctions) { f = (SqliteFunction)Activator.CreateInstance (pr._instanceType); f._base = sqlbase; f._InvokeFunc = (pr.FuncType == FunctionType.Scalar) ? new SQLiteCallback (f.ScalarCallback) : null; f._StepFunc = (pr.FuncType == FunctionType.Aggregate) ? new SQLiteCallback (f.StepCallback) : null; f._FinalFunc = (pr.FuncType == FunctionType.Aggregate) ? new SQLiteFinalCallback (f.FinalCallback) : null; f._CompareFunc = (pr.FuncType == FunctionType.Collation) ? new SQLiteCollation (f.CompareCallback) : null; f._CompareFunc16 = (pr.FuncType == FunctionType.Collation) ? new SQLiteCollation (f.CompareCallback16) : null;

if (pr.FuncType!= FunctionType.Collation) sqlbase.CreateFunction (pr.Name, pr.Arguments, (f is SqliteFunctionEx) , f._InvokeFunc, f._StepFunc, f._FinalFunc); else sqlbase.CreateCollation (pr.Name, f._CompareFunc, f._CompareFunc16); lFunctions.Add (f); } SqliteFunction[] arFunctions = new SqliteFunction[lFunctions.Count]; lFunctions.CopyTo (arFunctions, 0); return arFunctions; } } Обратите внимание на параметры, передаваемые в метод CreateFunction: они являются функциями обратного вызова и объявлены в классе SQLiteFunction. Например ScalarCallback: internal void ScalarCallback (IntPtr context, int nArgs, IntPtr argsptr) { _context = context; SetReturnValue (context, Invoke (ConvertParams (nArgs, argsptr))); } И этот метод не является статическим и не имеет атрибут MonoPInvokeCallbackAttribute. Причина ошибки обнаружена.Решение проблемы Рассмотрим несколько возможных способов решения: Через DllImport подключиться к SQLite и вызвать функцию sqlite3_create_function напрямую Использовать класс UnsafeNativeMethods, объявленный в Mono.Data.SQLite Использовать метод SQLite3.CreateFunction Все три метода по своему хороши, но я все таки пошел по третьему пути, так как он обещает минимальное вмешательство в работу системы.Исходные коды решения расположены на гитхабе.

Для начала нам необходимо получить экземпляр класса SQLite3 для текущего соединения. Согласно исходникам, его можно обнаружить в приватном поле _sql экземпляра класса SqliteConnection. Так что применяем рефлексию:

FieldInfo connection_sql = connection.GetType ().GetField (»_sql», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3 = connection_sql.GetValue (connection); где connection — экземпляр SqliteConnection.Получить доступ к CreateFunction так же не составляет проблем: MethodInfo CreateFunction = _sqlite3.GetType ().GetMethod («CreateFunction», BindingFlags.Instance | BindingFlags.NonPublic); Таким образом, мы можем передать нашу функцию обратного вызова: static void ToLowerCallback (IntPtr context, int nArgs, IntPtr argptr) { … } передавая экземпляр делегата SQLiteCallback: Type SQLiteCallbackDelegate = connection.GetType ().Assembly.GetType («Mono.Data.Sqlite.SQLiteCallback»); var callback = Delegate.CreateDelegate (SQLiteCallbackDelegate, typeof (DbFunctions).GetMethod («ToLowerCallback», BindingFlags.Static | BindingFlags.NonPublic)); CreateFunction.Invoke (_sqlite3, new object[] { «TOLOWER», 1, false, callback, null, null }); Но как же нам использовать атрибут MonoPInvokeCallback, если он требует соответствующий тип делегата в качестве параметра? Да как угодно! Обратите внимание на код: [AttributeUsage (AttributeTargets.Method)] sealed class MonoPInvokeCallbackAttribute: Attribute { public MonoPInvokeCallbackAttribute (Type t) {} } Получается, что абсолютно не важно, что мы будем передавать в конструктор атрибута? Нет, это не так: если передать typeof (object), то происходит ошибка AOT компиляции на устройстве. Так что просто создадим фальшивый делегат public delegate void FakeSQLiteCallback (IntPtr context, int nArgs, IntPtr argptr); и добавим атрибут [MonoPInvokeCallback (typeof (FakeSQLiteCallback))] static void ToLowerCallback (IntPtr context, int nArgs, IntPtr argptr) { … } Если скомпилировать код и попытаться использовать нашу функцию в запросе, вышеуказанный метод будет исправно вызываться.Осталось только добавить логику.Вернемся в исходные коды Mono.Data.Sqlite. Обратите внимание, как происходит взаимодействие с неуправляемым кодом в ScalarCallback: через методы ConvertParams и SetReturnValue. Безусловно, мы можем вызывать эти методы через рефлексию, но они не статические, так что понадобилось бы создавать экземпляр класса SQLiteFunction. Так что стоит попробовать просто повторить их логику в своем коде, используя рефлексию. Конечно, получение методов и полей достаточно дорогостоящая операция, так что необходимые FieldInfo и MethodInfo будем создавать при инициализации:

Инициализация Type sqlite3 = _sqlite3.GetType (); _sqlite3_GetParamValueType = sqlite3.GetMethod («GetParamValueType», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_GetParamValueInt64 = sqlite3.GetMethod («GetParamValueInt64», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_GetParamValueDouble = sqlite3.GetMethod («GetParamValueDouble», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_GetParamValueText = sqlite3.GetMethod («GetParamValueText», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_GetParamValueBytes = sqlite3.GetMethod («GetParamValueBytes», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ToDateTime = sqlite3.BaseType.GetMethod («ToDateTime», new Type[] { typeof (string) });

_sqlite3_ReturnNull = sqlite3.GetMethod («ReturnNull», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ReturnError = sqlite3.GetMethod («ReturnError», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ReturnInt64 = sqlite3.GetMethod («ReturnInt64», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ReturnDouble = sqlite3.GetMethod («ReturnDouble», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ReturnText = sqlite3.GetMethod («ReturnText», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ReturnBlob = sqlite3.GetMethod («ReturnBlob», BindingFlags.Instance | BindingFlags.NonPublic); _sqlite3_ToString = sqlite3.GetMethod («ToString», new Type[] { typeof (DateTime) }); _sqliteConvert_TypeToAffinity = typeof (SqliteConvert).GetMethod («TypeToAffinity», BindingFlags.Static | BindingFlags.NonPublic); Осталось просто создать необходимые методы: Методы PrepareParameters и ReturnValue static object[] PrepareParameters (int nArgs, IntPtr argptr) { object[] parms = new object[nArgs]; int[] argint = new int[nArgs]; Marshal.Copy (argptr, argint, 0, nArgs); for (int n = 0; n < nArgs; n++) { TypeAffinity affinity = (TypeAffinity)_sqlite3_GetParamValueType.InvokeSqlite ((IntPtr)argint [n]); switch (affinity) { case TypeAffinity.Null: parms [n] = DBNull.Value; break; case TypeAffinity.Int64: parms [n] = _sqlite3_GetParamValueInt64.InvokeSqlite ((IntPtr)argint [n]); break; case TypeAffinity.Double: parms [n] = _sqlite3_GetParamValueDouble.InvokeSqlite ((IntPtr)argint [n]); break; case TypeAffinity.Text: parms [n] = _sqlite3_GetParamValueText.InvokeSqlite ((IntPtr)argint [n]); break; case TypeAffinity.Blob: int x; byte[] blob; x = (int)_sqlite3_GetParamValueBytes.InvokeSqlite ((IntPtr)argint [n], 0, null, 0, 0); blob = new byte[x]; _sqlite3_GetParamValueBytes.InvokeSqlite ((IntPtr)argint [n], 0, blob, 0, 0); parms [n] = blob; break; case TypeAffinity.DateTime: object text = _sqlite3_GetParamValueText.InvokeSqlite ((IntPtr)argint [n]); parms [n] = _sqlite3_ToDateTime.InvokeSqlite (text); break; } } return parms; }

static void ReturnValue (IntPtr context, object result) { if (result == null || result == DBNull.Value) { _sqlite3_ReturnNull.Invoke (_sqlite3, new object[] { context }); return; }

Type t = result.GetType (); if (t == typeof (DateTime)) { object str = _sqlite3_ToString.InvokeSqlite (result); _sqlite3_ReturnText.InvokeSqlite (context, str); return; } else { Exception r = result as Exception; if (r!= null) { _sqlite3_ReturnError.InvokeSqlite (context, r.Message); return; } } TypeAffinity resultAffinity = (TypeAffinity)_sqliteConvert_TypeToAffinity.InvokeSqlite (t); switch (resultAffinity) { case TypeAffinity.Null: _sqlite3_ReturnNull.InvokeSqlite (context); return; case TypeAffinity.Int64: _sqlite3_ReturnInt64.InvokeSqlite (context, Convert.ToInt64 (result)); return; case TypeAffinity.Double: _sqlite3_ReturnDouble.InvokeSqlite (context, Convert.ToDouble (result)); return; case TypeAffinity.Text: _sqlite3_ReturnText.InvokeSqlite (context, result.ToString ()); return; case TypeAffinity.Blob: _sqlite3_ReturnBlob.InvokeSqlite (context, (byte[])result); return; } }

static object InvokeSqlite (this MethodInfo mi, params object[] parameters) { return mi.Invoke (_sqlite3, parameters); } В итоге, наша функция обратного вызова принимает окончательный вид: [MonoPInvokeCallback (typeof (FakeSQLiteCallback))] static void ToLowerCallback (IntPtr context, int nArgs, IntPtr argptr) { object[] parms = PrepareParameters (nArgs, argptr); object result = parms [0].ToString ().ToLower (); ReturnValue (context, result); } Вывод Благодаря наличию исходных текстов и существовании рефлексии нам удалось обойти платформенные ограничения и получить необходимый функционал. Как я выше уже писал, пример решения выложен на гитхабе.Я не считаю это решение единственно верным, но оно работает. Так же я был бы рад увидеть в комментариях ваш вариант.

© Habrahabr.ru