Работа с детищем Xamarin интересна и полна сюрпризов, как в хорошем смысле слова, так и в плохом. Одни проблемы решаются при помощи гугла и StackOverflow, другие же требуют нестандартного подхода. В данной статье я хочу рассказать историю о том, как можно с помощью исходников, рефлексии и трех кружек чая решить одну пренеприятнейшую проблему.
А проблема заключается в том, что Monotouch не поддерживает пользовательские функции в SQLite. Попытка подключить их через стандартный API приводит к ошибке вида:
Для начала прочитаем статью http://docs.xamarin.com/ios/about/limitations. Так как мы передаем функцию обратного вызова, то начинают играть роль Reverce Callbacks ограничения:
Сам метод sqlite3_create_function вызывается из SQLite3.CreateFunction():
Рассмотрим несколько возможных способов решения:
Исходные коды решения расположены на гитхабе: https://github.com/AleksandrKugushev/MonotouchSqliteFunctionsSample
Для начала нам необходимо получить экземпляр класса SQLite3 для текущего соединения. Согласно исходникам, его можно обнаружить в приватном поле _sql экземпляра класса SqliteConnection. Так что применяем рефлексию:
Получить доступ к CreateFunction так же не составляет проблем:
Осталось только добавить логику.
Вернемся в исходные коды Mono.Data.Sqlite. Обратите внимание, как происходит взаимодействие с неуправляемым кодом в ScalarCallback: через методы ConvertParams и SetReturnValue. Безусловно, мы можем вызывать эти методы через рефлексию, но они не статические, так что понадобилось бы создавать экземпляр класса SQLiteFunction. Так что стоит попробовать просто повторить их логику в своем коде, используя рефлексию. Конечно, получение методов и полей достаточно дорогостоящая операция, так что необходимые FieldInfo и MethodInfo будем создавать при инициализации:
Я не считаю это решение единственно верным, но оно работает. Так же я был бы рад увидеть в комментариях ваш вариант.
А проблема заключается в том, что 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.
Поиск причины
Для начала прочитаем статью http://docs.xamarin.com/ios/about/limitations. Так как мы передаем функцию обратного вызова, то начинают играть роль Reverce Callbacks ограничения:
- Метод должен иметь атрибут MonoPInvokeCallbackAttribute
- Он должен быть статическими
Сам метод sqlite3_create_function вызывается из SQLite3.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:internal static SqliteFunction[] BindFunctions(SQLiteBase sqlbase)
{
SqliteFunction f;
List<SqliteFunction> lFunctions = new List<SqliteFunction>();
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: они являются функциями обратного вызова и объявлены в классе 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
Исходные коды решения расположены на гитхабе: https://github.com/AleksandrKugushev/MonotouchSqliteFunctionsSample
Для начала нам необходимо получить экземпляр класса 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);
Осталось просто создать необходимые методы: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);
}
Вывод
Благодаря наличию исходных текстов и существовании рефлексии нам удалось обойти платформенные ограничения и получить необходимый функционал. Как я выше уже писал, пример решения выложен на гитхаб: https://github.com/AleksandrKugushev/MonotouchSqliteFunctionsSampleЯ не считаю это решение единственно верным, но оно работает. Так же я был бы рад увидеть в комментариях ваш вариант.
Комментариев нет:
Отправить комментарий