SQLiteをMonotouchまたはリフレクションの実用化で扱います

Xamarinの発案者と協力することは、言葉の良識と悪意の両方において、興味深く驚きに満ちています。 一部の問題はGoogleとStackOverflowを使用して解決されますが、他の問題は非標準的なアプローチが必要です。 この記事では、ソースコード、リフレクション、3杯のお茶の助けを借りて、1つの不快な問題をどのように解決できるかについて話をしたいと思います。





そして問題は、MonotouchがSQLiteのカスタム関数をサポートしていないことです。 標準APIを介してそれらを接続しようとすると、次の形式のエラーが発生します。

--aot-onlyで実行中にメソッド '(ラッパーネイティブから管理対象)Mono.Data.Sqlite.SqliteFunction:ScalarCallback(intptr、int、intptr)'をJITコンパイルしようとしています。 詳細については、http://docs.xamarin.com/ios/about/limitationsを参照してください。


そして、これはモノソースに行く必要があることを意味します。これは今から行います: https : //github.com/mono/mono SQLiteコードは、パスに沿って配置されます:\ mono \ mcs \ class \ Mono.Data.Sqlite \ Mono.Data.Sqlite_2.0。



理由を検索する


開始するには、モノタッチプラットフォームの制限に関する記事をご覧ください 。 コールバック関数を渡すため、制限がReverce Callbacksの役割を果たし始めます。

  1. メソッドにはMonoPInvokeCallbackAttribute属性が必要です。
  2. 静的でなければなりません


次に、ネイティブSQLite APIにアクセスするためのコード、つまりUnsafeNativeMethodsクラスを見つけます。 SQLiteのマニュアルには、関数を接続するにはsqlite3_create_functionメソッドを呼び出す必要があると書かれています。 もちろん、DllImportを介して呼び出されます。 明らかに、コールバック関数はパラメーターの1つとして渡されます。

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で使用されます。

バインド関数メソッド
 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メソッドに渡されるパラメーターに注意してください。これらはコールバック関数であり、SQLiteFunctionクラスで宣言されています。 たとえば、ScalarCallback:

  internal void ScalarCallback(IntPtr context, int nArgs, IntPtr argsptr) { _context = context; SetReturnValue(context, Invoke(ConvertParams(nArgs, argsptr))); }
      
      





また、このメソッドは静的ではなく、MonoPInvokeCallbackAttribute属性を持ちません。 エラーの原因が検出されました。



問題解決


考えられるいくつかの解決策を検討してください。

  1. DllImportを介してSQLiteに接続し、sqlite3_create_function関数を直接呼び出します
  2. Mono.Data.SQLiteで宣言されたUnsafeNativeMethodsクラスを使用します
  3. SQLite3.CreateFunctionメソッドを使用する


3つの方法はすべて独自の方法で優れていますが、システムへの干渉を最小限に抑えることができるため、3番目の方法を使用しました。



ソリューションのソースコードはgithubにあります。



最初に、現在の接続のSQLite3クラスのインスタンスを取得する必要があります。 ソースによると、SqliteConnectionクラスのインスタンスのprivate _sqlフィールドにあります。 そこで、反射を適用します。

 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(オブジェクト)が渡されると、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のソースコードに戻りましょう。 ConvertParamsおよびSetReturnValueメソッドを使用して、ScalarCallbackでアンマネージコードとの対話がどのように発生するかに注意してください。 もちろん、これらのメソッドはリフレクションを通じて呼び出すことができますが、静的ではないため、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); }
      
      







おわりに


ソーステキストとリフレクションの存在により、プラットフォームの制限を回避し、必要な機能を取得することができました。 上記で書いたように、ソリューションの例がgithubに投稿されています。

このソリューションが唯一の正しいソリューションだとは思いませんが、うまくいきます。 また、コメントであなたの選択肢を見てうれしいです。



All Articles