From f135a498615f000a1339d71419a84581a95bfb6e Mon Sep 17 00:00:00 2001 From: Daco Harkes Date: Tue, 26 Mar 2019 14:46:09 +0000 Subject: [PATCH] [doc] dart:ffi SQLite sample Issue: https://github.com/dart-lang/sdk/issues/35775 Change-Id: I2ce86c554ffd6f49050cf63ead60809c08fb02e5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/97504 Reviewed-by: Michael Thomsen --- samples/ffi/sqlite/.gitignore | 7 + samples/ffi/sqlite/README.md | 41 ++ .../ffi/sqlite/docs/lib/scenario-default.svg | 130 ++++++ samples/ffi/sqlite/docs/lib/scenario-full.svg | 149 +++++++ samples/ffi/sqlite/docs/sqlite-tutorial.md | 234 +++++++++++ samples/ffi/sqlite/lib/sqlite.dart | 6 + .../ffi/sqlite/lib/src/bindings/bindings.dart | 388 ++++++++++++++++++ .../sqlite/lib/src/bindings/constants.dart | 178 ++++++++ .../sqlite/lib/src/bindings/signatures.dart | 51 +++ .../ffi/sqlite/lib/src/bindings/types.dart | 73 ++++ .../src/collections/closable_iterator.dart | 25 ++ samples/ffi/sqlite/lib/src/database.dart | 303 ++++++++++++++ samples/ffi/sqlite/lib/src/ffi/arena.dart | 53 +++ samples/ffi/sqlite/lib/src/ffi/cstring.dart | 39 ++ .../ffi/sqlite/lib/src/ffi/dylib_utils.dart | 19 + samples/ffi/sqlite/pubspec.yaml | 9 + samples/ffi/sqlite/test/sqlite_test.dart | 160 ++++++++ 17 files changed, 1865 insertions(+) create mode 100644 samples/ffi/sqlite/.gitignore create mode 100644 samples/ffi/sqlite/README.md create mode 100644 samples/ffi/sqlite/docs/lib/scenario-default.svg create mode 100644 samples/ffi/sqlite/docs/lib/scenario-full.svg create mode 100644 samples/ffi/sqlite/docs/sqlite-tutorial.md create mode 100644 samples/ffi/sqlite/lib/sqlite.dart create mode 100644 samples/ffi/sqlite/lib/src/bindings/bindings.dart create mode 100644 samples/ffi/sqlite/lib/src/bindings/constants.dart create mode 100644 samples/ffi/sqlite/lib/src/bindings/signatures.dart create mode 100644 samples/ffi/sqlite/lib/src/bindings/types.dart create mode 100644 samples/ffi/sqlite/lib/src/collections/closable_iterator.dart create mode 100644 samples/ffi/sqlite/lib/src/database.dart create mode 100644 samples/ffi/sqlite/lib/src/ffi/arena.dart create mode 100644 samples/ffi/sqlite/lib/src/ffi/cstring.dart create mode 100644 samples/ffi/sqlite/lib/src/ffi/dylib_utils.dart create mode 100644 samples/ffi/sqlite/pubspec.yaml create mode 100644 samples/ffi/sqlite/test/sqlite_test.dart diff --git a/samples/ffi/sqlite/.gitignore b/samples/ffi/sqlite/.gitignore new file mode 100644 index 00000000000..7a6bc2eeab3 --- /dev/null +++ b/samples/ffi/sqlite/.gitignore @@ -0,0 +1,7 @@ +.dart_tool +.gdb_history +.packages +.vscode +pubspec.lock +test.db +test.db-journal \ No newline at end of file diff --git a/samples/ffi/sqlite/README.md b/samples/ffi/sqlite/README.md new file mode 100644 index 00000000000..6659300cf8f --- /dev/null +++ b/samples/ffi/sqlite/README.md @@ -0,0 +1,41 @@ +# Sample code dart:ffi + +This is an illustrative sample for how to use `dart:ffi`. + + +## Building and Running this Sample + +Building and running this sample is done through pub. +Running `pub get` and `pub run test` should produce the following output. + +```sh +$ pub get +Resolving dependencies... (6.8s) ++ analyzer 0.35.4 +... ++ yaml 2.1.15 +Downloading analyzer 0.35.4... +Downloading kernel 0.3.14... +Downloading front_end 0.1.14... +Changed 47 dependencies! +Precompiling executables... (18.0s) +Precompiled test:test. + +``` + +``` +$ pub run test +00:01 +0: test/sqlite_test.dart: sqlite integration test +1 Chocolade chip cookie Chocolade cookie foo +2 Ginger cookie null 42 +3 Cinnamon roll null null +1 Chocolade chip cookie Chocolade cookie foo +2 Ginger cookie null 42 +expected exception on accessing result data after close: The result has already been closed. +expected this query to fail: no such column: non_existing_column (Code 1: SQL logic error) +00:02 +3: All tests passed! +``` + +## Tutorial + +A tutorial walking through the code is available in [docs/sqlite-tutorial.md](docs/sqlite-tutorial.md). \ No newline at end of file diff --git a/samples/ffi/sqlite/docs/lib/scenario-default.svg b/samples/ffi/sqlite/docs/lib/scenario-default.svg new file mode 100644 index 00000000000..6ffa8a340a5 --- /dev/null +++ b/samples/ffi/sqlite/docs/lib/scenario-default.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + Produced by OmniGraffle 7.9.4 + 2019-03-13 09:56:08 +0000 + + + Canvas 1 + + + Layer 1 + + + + + Flutter + App + (Imports + package) + + + + + + + Native + Library + + + + + + + dart:ffi + + + + + + + Package + API + (Does not + expose + dart:ffi) + + + + + + + Dart + + + + + + + C / C++ + + + + + + + App + Developer + + + + + + + Package + Developer + + + + + + + Dart + VM + Team + + + + + + + Bindings + + + + + + + Native + Library + Developer + + + + + + + Package + Implementation + (Code which + converts C++ + abstractions into + Dart + abstractions) + + + + + diff --git a/samples/ffi/sqlite/docs/lib/scenario-full.svg b/samples/ffi/sqlite/docs/lib/scenario-full.svg new file mode 100644 index 00000000000..4ae18c501a8 --- /dev/null +++ b/samples/ffi/sqlite/docs/lib/scenario-full.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + Produced by OmniGraffle 7.9.4 + 2019-03-13 09:53:08 +0000 + + + Canvas 1 + + + Layer 1 + + + + + Flutter + App + (Imports + package) + + + + + + + Native + Library + + + + + + + dart:ffi + + + + + + + Package + API + (Does not + expose + dart:ffi) + + + + + + + Dart + + + + + + + C / C++ + + + + + + + App + Developer + + + + + + + Package + Developer + + + + + + + Dart + VM + Team + + + + + + + Bindings + + + + + + + Native + Library + Developer + + + + + + + Package + Implementation + (Code which + converts C++ + abstractions into + Dart + abstractions) + + + + + + + Package + Developer + + + + + + + Glue code + (Code which + takes care of + things such as + C++ exceptions) + + + + + diff --git a/samples/ffi/sqlite/docs/sqlite-tutorial.md b/samples/ffi/sqlite/docs/sqlite-tutorial.md new file mode 100644 index 00000000000..7f7aa22a30b --- /dev/null +++ b/samples/ffi/sqlite/docs/sqlite-tutorial.md @@ -0,0 +1,234 @@ +# dart:ffi SQLite mini tutorial + +In this mini tutorial we learn how to bind SQLite, a native library, in Dart using Dart's new foreign function interface `dart:ffi`. +We build a package which provides a Dartlike SQLite API using objects and `Iterator`s. +Inside the package we write Dart code which directly invokes C functions and manipulates C memory. + +## Binding C Functions to Dart + +The first step is to load a Native Library: + +```dart +import "dart:ffi"; + +DynamicLibrary sqlite = dlopenPlatformSpecific("sqlite3"); +``` + +In a `DynamicLibrary` we can `lookup` functions. +Let's lookup the function `sqlite3_prepare_v2` in the SQLite library. +That function has the following signature in the library header file. + +```c++ +SQLITE_API int sqlite3_prepare_v2( + sqlite3 *db, /* Database handle */ + const char *zSql, /* SQL statement, UTF-8 encoded */ + int nByte, /* Maximum length of zSql in bytes. */ + sqlite3_stmt **ppStmt, /* OUT: Statement handle */ + const char **pzTail /* OUT: Pointer to unused portion of zSql */ +); +``` + +In order to lookup a function, we need a _C signature_ and a _Dart signature_. + +```dart +typedef sqlite3_prepare_v2_native_t = Int32 Function( + DatabasePointer database, + CString query, + Int32 nbytes, + Pointer statementOut, + Pointer tail); + +typedef Sqlite3_prepare_v2_t = int Function( + DatabasePointer database, + CString query, + int nbytes, + Pointer statementOut, + Pointer tail); +``` + +With these two signatures we can `lookup` the C function and expose it as a Dart function with `asFunction`. + +```dart +Sqlite3_prepare_v2_t sqlite3_prepare_v2 = sqlite + .lookup>("sqlite3_prepare_v2") + .asFunction(); +``` + +Browse the code: [platform specific dynamic library loading](../lib/src/ffi/dylib_utils.dart), [C signatures](../lib/src/bindings/signatures.dart), [Dart signatures and bindings](../lib/src/bindings/bindings.dart), and [dart:ffi dynamic library interface](../../../../sdk/lib/ffi/dynamic_library.dart). + +## Managing C Memory + +In order to call `sqlite3_prepare_v2` to prepare a SQLite statement before executing, we need to be able to pass C pointers to C functions. + +Database and Statement pointers are opaque pointers in the SQLite C API. +We specify these as classes extending `Pointer`. + +```dart +class DatabasePointer extends Pointer {} +class StatementPointer extends Pointer {} +``` + +Strings in C are pointers to character arrays. + +```dart +class CString extends Pointer {} +``` + +Pointers to C integers, floats, an doubles can be read from and written through to `dart:ffi`. +However, before we can write to C memory from dart, we need to `allocate` some memory. + +```dart +Pointer p = allocate(); // Infers type argument allocate(), and allocates 1 byte. +p.store(123); // Stores a Dart int into this C int8. +int v = p.load(); // Infers type argument p.load(), and loads a value from C memory. +``` + +Note that you can only load a Dart `int` from a C `Int8`. +Trying to load a Dart `double` will result in a runtime exception. + +We've almost modeled C Strings. +The last thing we need is to use this `Pointer` as an array. +We can do this by using `elementAt`. + +```dart +CString string = allocate(count: 4).cast(); // Allocates 4 bytes and casts it to a string. +string.store(73); // Stores 'F' at index 0. +string.elementAt(1).store(73); // Stores 'F' at index 1. +string.elementAt(2).store(70); // Stores 'I' at index 2. +string.elementAt(3).store(0); // Null terminates the string. +``` + +We wrap the above logic of allocating strings in the constructor `CString.allocate`. + +Now we have all ingredients to call `sqlite3_prepare_v2`. + +```dart +Pointer statementOut = allocate(); +CString queryC = CString.allocate(query); +int resultCode = sqlite3_prepare_v2( + _database, queryC, -1, statementOut, fromAddress(0)); +``` + +With `dart:ffi` we are responsible for freeing C memory that we allocate. +So after calling `sqlite3_prepare_v2` we read out the statement pointer, and free the statement pointer pointer and `CString` which held the query string. + +``` +StatementPointer statement = statementOut.load(); +statementOut.free(); +queryC.free(); +``` + +Browse the code: [CString class](../lib/src/ffi/cstring.dart), [code calling sqlite3_prepare_v2](../lib/src/database.dart#57), and [dart:ffi pointer interface](../../../../sdk/lib/ffi/ffi.dart). + +## Dart API + +We would like to present the users of our package with an object oriented API - not exposing any `dart:ffi` objects to them. + +The SQLite C API returns a cursor to the first row of a result after executing a query. +We can read out the columns of this row and move the cursor to the next row. +The most natural way to expose this in Dart is through an `Iterable`. +We provide our package users with the following API. + +```dart +class Result implements Iterable {} + +class Row { + dynamic readColumnByIndex(int columnIndex) {} + dynamic readColumn(String columnName) {} +} +``` + +However, this interface does not completely match the semantics of the C API. +When we start reading the next `Row`, we do no longer have access to the previous `Row`. +We can model this by letting a `Row` keep track if its current or not. + +```dart +class Row { + bool _isCurrentRow = true; + + dynamic readColumnByIndex(int columnIndex) { + if (!_isCurrentRow) { + throw Exception( + "This row is not the current row, reading data from the non-current" + " row is not supported by sqlite."); + } + // ... + } +} +``` + +A second mismatch between Dart and C is that in C we have to manually release resources. +After executing a query and reading its results we need to call `sqlite3_finalize(statement)`. + +We can take two approaches here, either we structure the API in such a way that users of our package (implicitly) release resources, or we use finalizers to release resources. +In this tutorial we take the first approach. + +If our users iterate over all `Row`s, we can implicitly finalize the statement after they are done with the last row. +However, if they decide they do not want to iterate over the whole result, they need to explicitly state this. +In this tutorial, we use the `ClosableIterator` abstraction for `Iterators` with backing resources that need to be `close`d. + +```dart +Result result = d.query(""" + select id, name + from Cookies + ;"""); +for (Row r in result) { + String name = r.readColumn("name"); + print(name); +} +// Implicitly closes the iterator. + +result = d.query(""" + select id, name + from Cookies + ;"""); +for (Row r in result) { + int id = r.readColumn("id"); + if (id == 1) { + result.close(); // Explicitly closes the iterator, releasing underlying resources. + break; + } +} +``` + +Browse the code: [Database, Result, Row](../lib/src/database.dart), and [CloseableIterator](../lib/src/collections/closable_iterator.dart). + +## Architecture Overview + +The following diagram summarized what we have implemented as _package developers_ in this tutorial. + +![architecture](lib/scenario-default.svg) + +As the package developers wrapping an existing native library, we have only written Dart code - not any C/C++ code. +We specified bindings to the native library. +We have provided our package users with an object oriented API without exposing any `dart:ffi` objects. +And finally, we have implemented the package API by calling the C API. + +## Current dart:ffi Development Status + +In this minitutorial we used these `dart:ffi` features: + +* Loading dynamic libararies and looking up C functions in these dynamic libraries. +* Calling C functions, with `dart:ffi` automatically marshalling arguments and return value. +* Manipulating C memory through `Pointer`s with `allocate`, `free`, `load`, `store`, and `elementAt`. + +Features which we did not use in this tutorial: + +* `@struct` on subtypes of `Pointer` to define a struct with fields. (However, this feature is likely to change in the future.) + +Features which `dart:ffi` does not support yet: + +* Callbacks from C back into Dart. +* Finalizers +* C++ Exceptions (Not on roadmap yet.) + +Platform limitations: + +* `dart:ffi` is only enabled on 64 bit Windows, Linux, and MacOS. (Arm64 and 32 bit Intel are under review.) +* `dart:ffi` only works in JIT mode, not in AOT. + +It is possible to work around some of the current limitations by adding a C/C++ layer. +For example we could catch C++ exceptions in a C++ layer, and rethrow them in Dart. +The architecture diagram would change to the following in that case. + +![architecture2](lib/scenario-full.svg) \ No newline at end of file diff --git a/samples/ffi/sqlite/lib/sqlite.dart b/samples/ffi/sqlite/lib/sqlite.dart new file mode 100644 index 00000000000..ae7bdfbda02 --- /dev/null +++ b/samples/ffi/sqlite/lib/sqlite.dart @@ -0,0 +1,6 @@ +/// A synchronous SQLite wrapper. +/// +/// Written using dart:ffi. +library sqlite; + +export "src/database.dart"; diff --git a/samples/ffi/sqlite/lib/src/bindings/bindings.dart b/samples/ffi/sqlite/lib/src/bindings/bindings.dart new file mode 100644 index 00000000000..e92743a4b37 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/bindings/bindings.dart @@ -0,0 +1,388 @@ +import "dart:ffi"; + +import "../ffi/cstring.dart"; +import "../ffi/dylib_utils.dart"; + +import "signatures.dart"; +import "types.dart"; + +class _SQLiteBindings { + DynamicLibrary sqlite; + + /// Opening A New Database Connection + /// + /// ^These routines open an SQLite database file as specified by the + /// filename argument. ^The filename argument is interpreted as UTF-8 for + /// sqlite3_open() and sqlite3_open_v2() and as UTF-16 in the native byte + /// order for sqlite3_open16(). ^(A database connection handle is usually + /// returned in *ppDb, even if an error occurs. The only exception is that + /// if SQLite is unable to allocate memory to hold the sqlite3 object, + /// a NULL will be written into *ppDb instead of a pointer to the sqlite3 + /// object.)^ ^(If the database is opened (and/or created) successfully, then + /// [SQLITE_OK] is returned. Otherwise an error code is returned.)^ ^The + /// [sqlite3_errmsg] or sqlite3_errmsg16() routines can be used to obtain + /// an English language description of the error following a failure of any + /// of the sqlite3_open() routines. + int Function(CString filename, Pointer databaseOut, + int flags, CString vfs) sqlite3_open_v2; + + int Function(DatabasePointer database) sqlite3_close_v2; + + /// Compiling An SQL Statement + /// + /// To execute an SQL query, it must first be compiled into a byte-code + /// program using one of these routines. + /// + /// The first argument, "db", is a database connection obtained from a + /// prior successful call to sqlite3_open, [sqlite3_open_v2] or + /// sqlite3_open16. The database connection must not have been closed. + /// + /// The second argument, "zSql", is the statement to be compiled, encoded + /// as either UTF-8 or UTF-16. The sqlite3_prepare() and sqlite3_prepare_v2() + /// interfaces use UTF-8, and sqlite3_prepare16() and sqlite3_prepare16_v2() + /// use UTF-16. + /// + /// ^If the nByte argument is less than zero, then zSql is read up to the + /// first zero terminator. ^If nByte is non-negative, then it is the maximum + /// number of bytes read from zSql. ^When nByte is non-negative, the + /// zSql string ends at either the first '\000' or '\u0000' character or + /// the nByte-th byte, whichever comes first. If the caller knows + /// that the supplied string is nul-terminated, then there is a small + /// performance advantage to be gained by passing an nByte parameter that + /// is equal to the number of bytes in the input string including + /// the nul-terminator bytes. + /// + /// ^If pzTail is not NULL then *pzTail is made to point to the first byte + /// past the end of the first SQL statement in zSql. These routines only + /// compile the first statement in zSql, so *pzTail is left pointing to + /// what remains uncompiled. + /// + /// ^*ppStmt is left pointing to a compiled prepared statement that can be + /// executed using sqlite3_step. ^If there is an error, *ppStmt is set + /// to NULL. ^If the input text contains no SQL (if the input is an empty + /// string or a comment) then *ppStmt is set to NULL. + /// The calling procedure is responsible for deleting the compiled + /// SQL statement using [sqlite3_finalize] after it has finished with it. + /// ppStmt may not be NULL. + /// + /// ^On success, the sqlite3_prepare family of routines return [SQLITE_OK]; + /// otherwise an error code is returned. + /// + /// The sqlite3_prepare_v2() and sqlite3_prepare16_v2() interfaces are + /// recommended for all new programs. The two older interfaces are retained + /// for backwards compatibility, but their use is discouraged. + /// ^In the "v2" interfaces, the prepared statement + /// that is returned (the sqlite3_stmt object) contains a copy of the + /// original SQL text. This causes the [sqlite3_step] interface to + /// behave differently in three ways: + int Function( + DatabasePointer database, + CString query, + int nbytes, + Pointer statementOut, + Pointer tail) sqlite3_prepare_v2; + + /// Evaluate An SQL Statement + /// + /// After a prepared statement has been prepared using either + /// [sqlite3_prepare_v2] or sqlite3_prepare16_v2() or one of the legacy + /// interfaces sqlite3_prepare() or sqlite3_prepare16(), this function + /// must be called one or more times to evaluate the statement. + /// + /// The details of the behavior of the sqlite3_step() interface depend + /// on whether the statement was prepared using the newer "v2" interface + /// [sqlite3_prepare_v2] and sqlite3_prepare16_v2() or the older legacy + /// interface sqlite3_prepare() and sqlite3_prepare16(). The use of the + /// new "v2" interface is recommended for new applications but the legacy + /// interface will continue to be supported. + /// + /// ^In the legacy interface, the return value will be either [SQLITE_BUSY], + /// [SQLITE_DONE], [SQLITE_ROW], [SQLITE_ERROR], or [SQLITE_MISUSE]. + /// ^With the "v2" interface, any of the other [result codes] or + /// [extended result codes] might be returned as well. + /// + /// ^[SQLITE_BUSY] means that the database engine was unable to acquire the + /// database locks it needs to do its job. ^If the statement is a [COMMIT] + /// or occurs outside of an explicit transaction, then you can retry the + /// statement. If the statement is not a [COMMIT] and occurs within an + /// explicit transaction then you should rollback the transaction before + /// continuing. + /// + /// ^[SQLITE_DONE] means that the statement has finished executing + /// successfully. sqlite3_step() should not be called again on this virtual + /// machine without first calling [sqlite3_reset()] to reset the virtual + /// machine back to its initial state. + /// + /// ^If the SQL statement being executed returns any data, then [SQLITE_ROW] + /// is returned each time a new row of data is ready for processing by the + /// caller. The values may be accessed using the [column access functions]. + /// sqlite3_step() is called again to retrieve the next row of data. + /// + /// ^[SQLITE_ERROR] means that a run-time error (such as a constraint + /// violation) has occurred. sqlite3_step() should not be called again on + /// the VM. More information may be found by calling [sqlite3_errmsg()]. + /// ^With the legacy interface, a more specific error code (for example, + /// [SQLITE_INTERRUPT], [SQLITE_SCHEMA], [SQLITE_CORRUPT], and so forth) + /// can be obtained by calling [sqlite3_reset()] on the + /// prepared statement. ^In the "v2" interface, + /// the more specific error code is returned directly by sqlite3_step(). + /// + /// [SQLITE_MISUSE] means that the this routine was called inappropriately. + /// Perhaps it was called on a prepared statement that has + /// already been [sqlite3_finalize | finalized] or on one that had + /// previously returned [SQLITE_ERROR] or [SQLITE_DONE]. Or it could + /// be the case that the same database connection is being used by two or + /// more threads at the same moment in time. + /// + /// For all versions of SQLite up to and including 3.6.23.1, a call to + /// [sqlite3_reset] was required after sqlite3_step() returned anything + /// other than [Errors.SQLITE_ROW] before any subsequent invocation of + /// sqlite3_step(). Failure to reset the prepared statement using + /// [sqlite3_reset()] would result in an [Errors.SQLITE_MISUSE] return from + /// sqlite3_step(). But after version 3.6.23.1, sqlite3_step() began + /// calling [sqlite3_reset] automatically in this circumstance rather + /// than returning [Errors.SQLITE_MISUSE]. This is not considered a + /// compatibility break because any application that ever receives an + /// [Errors.SQLITE_MISUSE] error is broken by definition. The + /// [SQLITE_OMIT_AUTORESET] compile-time option + /// can be used to restore the legacy behavior. + /// + /// Goofy Interface Alert: In the legacy interface, the sqlite3_step() + /// API always returns a generic error code, [SQLITE_ERROR], following any + /// error other than [SQLITE_BUSY] and [SQLITE_MISUSE]. You must call + /// [sqlite3_reset()] or [sqlite3_finalize()] in order to find one of the + /// specific [error codes] that better describes the error. + /// We admit that this is a goofy design. The problem has been fixed + /// with the "v2" interface. If you prepare all of your SQL statements + /// using either [sqlite3_prepare_v2()] or [sqlite3_prepare16_v2()] instead + /// of the legacy [sqlite3_prepare()] and [sqlite3_prepare16()] interfaces, + /// then the more specific [error codes] are returned directly + /// by sqlite3_step(). The use of the "v2" interface is recommended. + int Function(StatementPointer statement) sqlite3_step; + + /// CAPI3REF: Reset A Prepared Statement Object + /// + /// The sqlite3_reset() function is called to reset a prepared statement + /// object back to its initial state, ready to be re-executed. + /// ^Any SQL statement variables that had values bound to them using + /// the sqlite3_bind_blob | sqlite3_bind_*() API retain their values. + /// Use sqlite3_clear_bindings() to reset the bindings. + /// + /// ^The [sqlite3_reset] interface resets the prepared statement S + /// back to the beginning of its program. + /// + /// ^If the most recent call to [sqlite3_step] for the + /// prepared statement S returned [Errors.SQLITE_ROW] or [Errors.SQLITE_DONE], + /// or if [sqlite3_step] has never before been called on S, + /// then [sqlite3_reset] returns [Errors.SQLITE_OK]. + /// + /// ^If the most recent call to [sqlite3_step(S)] for the + /// prepared statement S indicated an error, then + /// [sqlite3_reset] returns an appropriate [Errors]. + /// + /// ^The [sqlite3_reset] interface does not change the values + int Function(StatementPointer statement) sqlite3_reset; + + /// Destroy A Prepared Statement Object + /// + /// ^The sqlite3_finalize() function is called to delete a prepared statement. + /// ^If the most recent evaluation of the statement encountered no errors + /// or if the statement is never been evaluated, then sqlite3_finalize() + /// returns SQLITE_OK. ^If the most recent evaluation of statement S failed, + /// then sqlite3_finalize(S) returns the appropriate error code or extended + /// error code. + /// + /// ^The sqlite3_finalize(S) routine can be called at any point during + /// the life cycle of prepared statement S: + /// before statement S is ever evaluated, after + /// one or more calls to [sqlite3_reset], or after any call + /// to [sqlite3_step] regardless of whether or not the statement has + /// completed execution. + /// + /// ^Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op. + /// + /// The application must finalize every prepared statement in order to avoid + /// resource leaks. It is a grievous error for the application to try to use + /// a prepared statement after it has been finalized. Any use of a prepared + /// statement after it has been finalized can result in undefined and + /// undesirable behavior such as segfaults and heap corruption. + int Function(StatementPointer statement) sqlite3_finalize; + + /// Number Of Columns In A Result Set + /// + /// ^Return the number of columns in the result set returned by the + /// prepared statement. ^This routine returns 0 if pStmt is an SQL + /// statement that does not return data (for example an [UPDATE]). + int Function(StatementPointer statement) sqlite3_column_count; + + /// Column Names In A Result Set + /// + /// ^These routines return the name assigned to a particular column + /// in the result set of a SELECT statement. ^The sqlite3_column_name() + /// interface returns a pointer to a zero-terminated UTF-8 string + /// and sqlite3_column_name16() returns a pointer to a zero-terminated + /// UTF-16 string. ^The first parameter is the prepared statement + /// that implements the SELECT statement. ^The second parameter is the + /// column number. ^The leftmost column is number 0. + /// + /// ^The returned string pointer is valid until either the prepared statement + /// is destroyed by [sqlite3_finalize] or until the statement is automatically + /// reprepared by the first call to [sqlite3_step] for a particular run + /// or until the next call to + /// sqlite3_column_name() or sqlite3_column_name16() on the same column. + /// + /// ^If sqlite3_malloc() fails during the processing of either routine + /// (for example during a conversion from UTF-8 to UTF-16) then a + /// NULL pointer is returned. + /// + /// ^The name of a result column is the value of the "AS" clause for + /// that column, if there is an AS clause. If there is no AS clause + /// then the name of the column is unspecified and may change from + CString Function(StatementPointer statement, int columnIndex) + sqlite3_column_name; + + /// CAPI3REF: Declared Datatype Of A Query Result + /// + /// ^(The first parameter is a prepared statement. + /// If this statement is a SELECT statement and the Nth column of the + /// returned result set of that SELECT is a table column (not an + /// expression or subquery) then the declared type of the table + /// column is returned.)^ ^If the Nth column of the result set is an + /// expression or subquery, then a NULL pointer is returned. + /// ^The returned string is always UTF-8 encoded. + /// + /// ^(For example, given the database schema: + /// + /// CREATE TABLE t1(c1 VARIANT); + /// + /// and the following statement to be compiled: + /// + /// SELECT c1 + 1, c1 FROM t1; + /// + /// this routine would return the string "VARIANT" for the second result + /// column (i==1), and a NULL pointer for the first result column (i==0).)^ + /// + /// ^SQLite uses dynamic run-time typing. ^So just because a column + /// is declared to contain a particular type does not mean that the + /// data stored in that column is of the declared type. SQLite is + /// strongly typed, but the typing is dynamic not static. ^Type + /// is associated with individual values, not with the containers + /// used to hold those values. + CString Function(StatementPointer statement, int columnIndex) + sqlite3_column_decltype; + + int Function(StatementPointer statement, int columnIndex) sqlite3_column_type; + + ValuePointer Function(StatementPointer statement, int columnIndex) + sqlite3_column_value; + + double Function(StatementPointer statement, int columnIndex) + sqlite3_column_double; + + int Function(StatementPointer statement, int columnIndex) sqlite3_column_int; + + CString Function(StatementPointer statement, int columnIndex) + sqlite3_column_text; + + /// The sqlite3_errstr() interface returns the English-language text that + /// describes the result code, as UTF-8. Memory to hold the error message + /// string is managed internally and must not be freed by the application. + CString Function(int code) sqlite3_errstr; + + /// Error Codes And Messages + /// + /// ^The sqlite3_errcode() interface returns the numeric [result code] or + /// [extended result code] for the most recent failed sqlite3_* API call + /// associated with a [database connection]. If a prior API call failed + /// but the most recent API call succeeded, the return value from + /// sqlite3_errcode() is undefined. ^The sqlite3_extended_errcode() + /// interface is the same except that it always returns the + /// [extended result code] even when extended result codes are + /// disabled. + /// + /// ^The sqlite3_errmsg() and sqlite3_errmsg16() return English-language + /// text that describes the error, as either UTF-8 or UTF-16 respectively. + /// ^(Memory to hold the error message string is managed internally. + /// The application does not need to worry about freeing the result. + /// However, the error string might be overwritten or deallocated by + /// subsequent calls to other SQLite interface functions.)^ + /// + /// When the serialized [threading mode] is in use, it might be the + /// case that a second error occurs on a separate thread in between + /// the time of the first error and the call to these interfaces. + /// When that happens, the second error will be reported since these + /// interfaces always report the most recent result. To avoid + /// this, each thread can obtain exclusive use of the [database connection] D + /// by invoking [sqlite3_mutex_enter]([sqlite3_db_mutex](D)) before beginning + /// to use D and invoking [sqlite3_mutex_leave]([sqlite3_db_mutex](D)) after + /// all calls to the interfaces listed here are completed. + /// + /// If an interface fails with SQLITE_MISUSE, that means the interface + /// was invoked incorrectly by the application. In that case, the + /// error code and message may or may not be set. + CString Function(DatabasePointer database) sqlite3_errmsg; + + _SQLiteBindings() { + sqlite = dlopenPlatformSpecific("sqlite3"); + sqlite3_open_v2 = sqlite + .lookup>("sqlite3_open_v2") + .asFunction(); + sqlite3_close_v2 = sqlite + .lookup>("sqlite3_close_v2") + .asFunction(); + sqlite3_prepare_v2 = sqlite + .lookup>( + "sqlite3_prepare_v2") + .asFunction(); + sqlite3_step = sqlite + .lookup>("sqlite3_step") + .asFunction(); + sqlite3_reset = sqlite + .lookup>("sqlite3_reset") + .asFunction(); + sqlite3_finalize = sqlite + .lookup>("sqlite3_finalize") + .asFunction(); + sqlite3_errstr = sqlite + .lookup>("sqlite3_errstr") + .asFunction(); + sqlite3_errmsg = sqlite + .lookup>("sqlite3_errmsg") + .asFunction(); + sqlite3_column_count = sqlite + .lookup>( + "sqlite3_column_count") + .asFunction(); + sqlite3_column_name = sqlite + .lookup>( + "sqlite3_column_name") + .asFunction(); + sqlite3_column_decltype = sqlite + .lookup>( + "sqlite3_column_decltype") + .asFunction(); + sqlite3_column_type = sqlite + .lookup>( + "sqlite3_column_type") + .asFunction(); + sqlite3_column_value = sqlite + .lookup>( + "sqlite3_column_value") + .asFunction(); + sqlite3_column_double = sqlite + .lookup>( + "sqlite3_column_double") + .asFunction(); + sqlite3_column_int = sqlite + .lookup>( + "sqlite3_column_int") + .asFunction(); + sqlite3_column_text = sqlite + .lookup>( + "sqlite3_column_text") + .asFunction(); + } +} + +_SQLiteBindings _cachedBindings; +_SQLiteBindings get bindings => _cachedBindings ??= _SQLiteBindings(); diff --git a/samples/ffi/sqlite/lib/src/bindings/constants.dart b/samples/ffi/sqlite/lib/src/bindings/constants.dart new file mode 100644 index 00000000000..fbcf9223505 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/bindings/constants.dart @@ -0,0 +1,178 @@ +/// Result Codes +/// +/// Many SQLite functions return an integer result code from the set shown +/// here in order to indicates success or failure. +/// +/// New error codes may be added in future versions of SQLite. +/// +/// See also: SQLITE_IOERR_READ | extended result codes, +/// sqlite3_vtab_on_conflict() SQLITE_ROLLBACK | result codes. +class Errors { + /// Successful result + static const int SQLITE_OK = 0; + + /// Generic error + static const int SQLITE_ERROR = 1; + + /// Internal logic error in SQLite + static const int SQLITE_INTERNAL = 2; + + /// Access permission denied + static const int SQLITE_PERM = 3; + + /// Callback routine requested an abort + static const int SQLITE_ABORT = 4; + + /// The database file is locked + static const int SQLITE_BUSY = 5; + + /// A table in the database is locked + static const int SQLITE_LOCKED = 6; + + /// A malloc() failed + static const int SQLITE_NOMEM = 7; + + /// Attempt to write a readonly database + static const int SQLITE_READONLY = 8; + + /// Operation terminated by sqlite3_interrupt() + static const int SQLITE_INTERRUPT = 9; + + /// Some kind of disk I/O error occurred + static const int SQLITE_IOERR = 10; + + /// The database disk image is malformed + static const int SQLITE_CORRUPT = 11; + + /// Unknown opcode in sqlite3_file_control() + static const int SQLITE_NOTFOUND = 12; + + /// Insertion failed because database is full + static const int SQLITE_FULL = 13; + + /// Unable to open the database file + static const int SQLITE_CANTOPEN = 14; + + /// Database lock protocol error + static const int SQLITE_PROTOCOL = 15; + + /// Internal use only + static const int SQLITE_EMPTY = 16; + + /// The database schema changed + static const int SQLITE_SCHEMA = 17; + + /// String or BLOB exceeds size limit + static const int SQLITE_TOOBIG = 18; + + /// Abort due to constraint violation + static const int SQLITE_CONSTRAINT = 19; + + /// Data type mismatch + static const int SQLITE_MISMATCH = 20; + + /// Library used incorrectly + static const int SQLITE_MISUSE = 21; + + /// Uses OS features not supported on host + static const int SQLITE_NOLFS = 22; + + /// Authorization denied + static const int SQLITE_AUTH = 23; + + /// Not used + static const int SQLITE_FORMAT = 24; + + /// 2nd parameter to sqlite3_bind out of range + static const int SQLITE_RANGE = 25; + + /// File opened that is not a database file + static const int SQLITE_NOTADB = 26; + + /// Notifications from sqlite3_log() + static const int SQLITE_NOTICE = 27; + + /// Warnings from sqlite3_log() + static const int SQLITE_WARNING = 28; + + /// sqlite3_step() has another row ready + static const int SQLITE_ROW = 100; + + /// sqlite3_step() has finished executing + static const int SQLITE_DONE = 101; +} + +/// Flags For File Open Operations +/// +/// These bit values are intended for use in the +/// 3rd parameter to the [sqlite3_open_v2()] interface and +/// in the 4th parameter to the [sqlite3_vfs.xOpen] method. +class Flags { + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_READONLY = 0x00000001; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_READWRITE = 0x00000002; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_CREATE = 0x00000004; + + /// VFS only + static const int SQLITE_OPEN_DELETEONCLOSE = 0x00000008; + + /// VFS only + static const int SQLITE_OPEN_EXCLUSIVE = 0x00000010; + + /// VFS only + static const int SQLITE_OPEN_AUTOPROXY = 0x00000020; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_URI = 0x00000040; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_MEMORY = 0x00000080; + + /// VFS only + static const int SQLITE_OPEN_MAIN_DB = 0x00000100; + + /// VFS only + static const int SQLITE_OPEN_TEMP_DB = 0x00000200; + + /// VFS only + static const int SQLITE_OPEN_TRANSIENT_DB = 0x00000400; + + /// VFS only + static const int SQLITE_OPEN_MAIN_JOURNAL = 0x00000800; + + /// VFS only + static const int SQLITE_OPEN_TEMP_JOURNAL = 0x00001000; + + /// VFS only + static const int SQLITE_OPEN_SUBJOURNAL = 0x00002000; + + /// VFS only + static const int SQLITE_OPEN_MASTER_JOURNAL = 0x00004000; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_NOMUTEX = 0x00008000; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_FULLMUTEX = 0x00010000; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_SHAREDCACHE = 0x00020000; + + /// Ok for sqlite3_open_v2() + static const int SQLITE_OPEN_PRIVATECACHE = 0x00040000; + + /// VFS only + static const int SQLITE_OPEN_WAL = 0x00080000; +} + +class Types { + static const int SQLITE_INTEGER = 1; + static const int SQLITE_FLOAT = 2; + static const int SQLITE_TEXT = 3; + static const int SQLITE_BLOB = 4; + static const int SQLITE_NULL = 5; +} diff --git a/samples/ffi/sqlite/lib/src/bindings/signatures.dart b/samples/ffi/sqlite/lib/src/bindings/signatures.dart new file mode 100644 index 00000000000..06de8383282 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/bindings/signatures.dart @@ -0,0 +1,51 @@ +import "dart:ffi"; + +import "../ffi/cstring.dart"; + +import "types.dart"; + +typedef sqlite3_open_v2_native_t = Int32 Function( + CString filename, Pointer ppDb, Int32 flags, CString vfs); + +typedef sqlite3_close_v2_native_t = Int32 Function(DatabasePointer database); + +typedef sqlite3_prepare_v2_native_t = Int32 Function( + DatabasePointer database, + CString query, + Int32 nbytes, + Pointer statementOut, + Pointer tail); + +typedef sqlite3_step_native_t = Int32 Function(StatementPointer statement); + +typedef sqlite3_reset_native_t = Int32 Function(StatementPointer statement); + +typedef sqlite3_finalize_native_t = Int32 Function(StatementPointer statement); + +typedef sqlite3_errstr_native_t = CString Function(Int32 error); + +typedef sqlite3_errmsg_native_t = CString Function(DatabasePointer database); + +typedef sqlite3_column_count_native_t = Int32 Function( + StatementPointer statement); + +typedef sqlite3_column_name_native_t = CString Function( + StatementPointer statement, Int32 columnIndex); + +typedef sqlite3_column_decltype_native_t = CString Function( + StatementPointer statement, Int32 columnIndex); + +typedef sqlite3_column_type_native_t = Int32 Function( + StatementPointer statement, Int32 columnIndex); + +typedef sqlite3_column_value_native_t = ValuePointer Function( + StatementPointer statement, Int32 columnIndex); + +typedef sqlite3_column_double_native_t = Double Function( + StatementPointer statement, Int32 columnIndex); + +typedef sqlite3_column_int_native_t = Int32 Function( + StatementPointer statement, Int32 columnIndex); + +typedef sqlite3_column_text_native_t = CString Function( + StatementPointer statement, Int32 columnIndex); diff --git a/samples/ffi/sqlite/lib/src/bindings/types.dart b/samples/ffi/sqlite/lib/src/bindings/types.dart new file mode 100644 index 00000000000..9cb709c6ef9 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/bindings/types.dart @@ -0,0 +1,73 @@ +import "dart:ffi"; + +import "../ffi/cstring.dart"; + +/// Database Connection Handle +/// +/// Each open SQLite database is represented by a pointer to an instance of +/// the opaque structure named "sqlite3". It is useful to think of an sqlite3 +/// pointer as an object. The [sqlite3_open()], [sqlite3_open16()], and +/// [sqlite3_open_v2()] interfaces are its constructors, and [sqlite3_close()] +/// is its destructor. There are many other interfaces (such as +/// [sqlite3_prepare_v2()], [sqlite3_create_function()], and +/// [sqlite3_busy_timeout()] to name but three) that are methods on an +class DatabasePointer extends Pointer {} + +/// SQL Statement Object +/// +/// An instance of this object represents a single SQL statement. +/// This object is variously known as a "prepared statement" or a +/// "compiled SQL statement" or simply as a "statement". +/// +/// The life of a statement object goes something like this: +/// +///
    +///
  1. Create the object using [sqlite3_prepare_v2()] or a related +/// function. +///
  2. Bind values to [host parameters] using the sqlite3_bind_*() +/// interfaces. +///
  3. Run the SQL by calling [sqlite3_step()] one or more times. +///
  4. Reset the statement using [sqlite3_reset()] then go back +/// to step 2. Do this zero or more times. +///
  5. Destroy the object using [sqlite3_finalize()]. +///
+/// +/// Refer to documentation on individual methods above for additional +/// information. +class StatementPointer extends Pointer {} + +/// Dynamically Typed Value Object +/// +/// SQLite uses the sqlite3_value object to represent all values +/// that can be stored in a database table. SQLite uses dynamic typing +/// for the values it stores. ^Values stored in sqlite3_value objects +/// can be integers, floating point values, strings, BLOBs, or NULL. +/// +/// An sqlite3_value object may be either "protected" or "unprotected". +/// Some interfaces require a protected sqlite3_value. Other interfaces +/// will accept either a protected or an unprotected sqlite3_value. +/// Every interface that accepts sqlite3_value arguments specifies +/// whether or not it requires a protected sqlite3_value. +/// +/// The terms "protected" and "unprotected" refer to whether or not +/// a mutex is held. An internal mutex is held for a protected +/// sqlite3_value object but no mutex is held for an unprotected +/// sqlite3_value object. If SQLite is compiled to be single-threaded +/// (with [SQLITE_THREADSAFE=0] and with [sqlite3_threadsafe()] returning 0) +/// or if SQLite is run in one of reduced mutex modes +/// [SQLITE_CONFIG_SINGLETHREAD] or [SQLITE_CONFIG_MULTITHREAD] +/// then there is no distinction between protected and unprotected +/// sqlite3_value objects and they can be used interchangeably. However, +/// for maximum code portability it is recommended that applications +/// still make the distinction between protected and unprotected +/// sqlite3_value objects even when not strictly required. +/// +/// ^The sqlite3_value objects that are passed as parameters into the +/// implementation of [application-defined SQL functions] are protected. +/// ^The sqlite3_value object returned by +/// [sqlite3_column_value()] is unprotected. +/// Unprotected sqlite3_value objects may only be used with +/// [sqlite3_result_value()] and [sqlite3_bind_value()]. +/// The [sqlite3_value_blob | sqlite3_value_type()] family of +/// interfaces require protected sqlite3_value objects. +class ValuePointer extends Pointer {} diff --git a/samples/ffi/sqlite/lib/src/collections/closable_iterator.dart b/samples/ffi/sqlite/lib/src/collections/closable_iterator.dart new file mode 100644 index 00000000000..d5a05232574 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/collections/closable_iterator.dart @@ -0,0 +1,25 @@ +/// This iterator should be [close]d after use. +/// +/// [ClosableIterator]s often use resources which should be freed after use. +/// The consumer of the iterator can either manually [close] the iterator, or +/// consume all elements on which the iterator will automatically be closed. +abstract class ClosableIterator extends Iterator { + /// Close this iterator. + void close(); + + /// Moves to the next element and [close]s the iterator if it was the last + /// element. + bool moveNext(); +} + +/// This iterable's iterator should be [close]d after use. +/// +/// Companion class of [ClosableIterator]. +abstract class ClosableIterable extends Iterable { + /// Close this iterables iterator. + void close(); + + /// Returns a [ClosableIterator] that allows iterating the elements of this + /// [ClosableIterable]. + ClosableIterator get iterator; +} diff --git a/samples/ffi/sqlite/lib/src/database.dart b/samples/ffi/sqlite/lib/src/database.dart new file mode 100644 index 00000000000..11c21408619 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/database.dart @@ -0,0 +1,303 @@ +import "dart:collection"; +import "dart:ffi"; + +import "bindings/bindings.dart"; +import "bindings/types.dart"; +import "bindings/constants.dart"; +import "collections/closable_iterator.dart"; +import "ffi/cstring.dart"; + +/// [Database] represents an open connection to a SQLite database. +/// +/// All functions against a database may throw [SQLiteError]. +/// +/// This database interacts with SQLite synchonously. +class Database { + DatabasePointer _database; + bool _open = false; + + /// Open a database located at the file [path]. + Database(String path, + [int flags = Flags.SQLITE_OPEN_READWRITE | Flags.SQLITE_OPEN_CREATE]) { + Pointer dbOut = allocate(); + CString pathC = CString.allocate(path); + final int resultCode = + bindings.sqlite3_open_v2(pathC, dbOut, flags, fromAddress(0)); + _database = dbOut.load(); + dbOut.free(); + pathC.free(); + + if (resultCode == Errors.SQLITE_OK) { + _open = true; + } else { + // Even if "open" fails, sqlite3 will still create a database object. We + // can just destroy it. + SQLiteException exception = _loadError(resultCode); + close(); + throw exception; + } + } + + /// Close the database. + /// + /// This should only be called once on a database unless an exception is + /// thrown. It should be called at least once to finalize the database and + /// avoid resource leaks. + void close() { + assert(_open); + final int resultCode = bindings.sqlite3_close_v2(_database); + if (resultCode == Errors.SQLITE_OK) { + _open = false; + } else { + throw _loadError(resultCode); + } + } + + /// Execute a query, discarding any returned rows. + void execute(String query) { + Pointer statementOut = allocate(); + CString queryC = CString.allocate(query); + int resultCode = bindings.sqlite3_prepare_v2( + _database, queryC, -1, statementOut, fromAddress(0)); + StatementPointer statement = statementOut.load(); + statementOut.free(); + queryC.free(); + + while (resultCode == Errors.SQLITE_ROW || resultCode == Errors.SQLITE_OK) { + resultCode = bindings.sqlite3_step(statement); + } + bindings.sqlite3_finalize(statement); + if (resultCode != Errors.SQLITE_DONE) { + throw _loadError(resultCode); + } + } + + /// Evaluate a query and return the resulting rows as an iterable. + Result query(String query) { + Pointer statementOut = allocate(); + CString queryC = CString.allocate(query); + int resultCode = bindings.sqlite3_prepare_v2( + _database, queryC, -1, statementOut, fromAddress(0)); + StatementPointer statement = statementOut.load(); + statementOut.free(); + queryC.free(); + + if (resultCode != Errors.SQLITE_OK) { + bindings.sqlite3_finalize(statement); + throw _loadError(resultCode); + } + + Map columnIndices = {}; + int columnCount = bindings.sqlite3_column_count(statement); + for (int i = 0; i < columnCount; i++) { + String columnName = + CString.fromUtf8(bindings.sqlite3_column_name(statement, i)); + columnIndices[columnName] = i; + } + + return Result._(this, statement, columnIndices); + } + + SQLiteException _loadError([int errorCode]) { + String errorMessage = CString.fromUtf8(bindings.sqlite3_errmsg(_database)); + if (errorCode == null) { + return SQLiteException(errorMessage); + } + String errorCodeExplanation = + CString.fromUtf8(bindings.sqlite3_errstr(errorCode)); + return SQLiteException( + "$errorMessage (Code $errorCode: $errorCodeExplanation)"); + } +} + +/// [Result] represents a [Database.query]'s result and provides an [Iterable] +/// interface for the results to be consumed. +/// +/// Please note that this iterator should be [close]d manually if not all [Row]s +/// are consumed. +class Result extends IterableBase implements ClosableIterable { + final Database _database; + final ClosableIterator _iterator; + final StatementPointer _statement; + final Map _columnIndices; + + Row _currentRow = null; + + Result._( + this._database, + this._statement, + this._columnIndices, + ) : _iterator = _ResultIterator(_statement, _columnIndices) {} + + void close() => _iterator.close(); + + ClosableIterator get iterator => _iterator; +} + +class _ResultIterator implements ClosableIterator { + final StatementPointer _statement; + final Map _columnIndices; + + Row _currentRow = null; + bool _closed = false; + + _ResultIterator(this._statement, this._columnIndices) {} + + bool moveNext() { + if (_closed) { + throw SQLiteException("The result has already been closed."); + } + _currentRow?._setNotCurrent(); + int stepResult = bindings.sqlite3_step(_statement); + if (stepResult == Errors.SQLITE_ROW) { + _currentRow = Row._(_statement, _columnIndices); + return true; + } else { + close(); + return false; + } + } + + Row get current { + if (_closed) { + throw SQLiteException("The result has already been closed."); + } + return _currentRow; + } + + void close() { + _currentRow?._setNotCurrent(); + _closed = true; + bindings.sqlite3_finalize(_statement); + } +} + +class Row { + final StatementPointer _statement; + final Map _columnIndices; + + bool _isCurrentRow = true; + + Row._(this._statement, this._columnIndices) {} + + /// Reads column [columnName]. + /// + /// By default it returns a dynamically typed value. If [convert] is set to + /// [Convert.StaticType] the value is converted to the static type computed + /// for the column by the query compiler. + dynamic readColumn(String columnName, + {Convert convert = Convert.DynamicType}) { + return readColumnByIndex(_columnIndices[columnName], convert: convert); + } + + /// Reads column [columnName]. + /// + /// By default it returns a dynamically typed value. If [convert] is set to + /// [Convert.StaticType] the value is converted to the static type computed + /// for the column by the query compiler. + dynamic readColumnByIndex(int columnIndex, + {Convert convert = Convert.DynamicType}) { + _checkIsCurrentRow(); + + Type dynamicType; + if (convert == Convert.DynamicType) { + dynamicType = + _typeFromCode(bindings.sqlite3_column_type(_statement, columnIndex)); + } else { + dynamicType = _typeFromText(CString.fromUtf8( + bindings.sqlite3_column_decltype(_statement, columnIndex))); + } + + switch (dynamicType) { + case Type.Integer: + return readColumnByIndexAsInt(columnIndex); + case Type.Text: + return readColumnByIndexAsText(columnIndex); + case Type.Null: + return null; + break; + default: + } + } + + /// Reads column [columnName] and converts to [Type.Integer] if not an + /// integer. + int readColumnAsInt(String columnName) { + return readColumnByIndexAsInt(_columnIndices[columnName]); + } + + /// Reads column [columnIndex] and converts to [Type.Integer] if not an + /// integer. + int readColumnByIndexAsInt(int columnIndex) { + _checkIsCurrentRow(); + return bindings.sqlite3_column_int(_statement, columnIndex); + } + + /// Reads column [columnName] and converts to [Type.Text] if not text. + String readColumnAsText(String columnName) { + return readColumnByIndexAsText(_columnIndices[columnName]); + } + + /// Reads column [columnIndex] and converts to [Type.Text] if not text. + String readColumnByIndexAsText(int columnIndex) { + _checkIsCurrentRow(); + return CString.fromUtf8( + bindings.sqlite3_column_text(_statement, columnIndex)); + } + + void _checkIsCurrentRow() { + if (!_isCurrentRow) { + throw Exception( + "This row is not the current row, reading data from the non-current" + " row is not supported by sqlite."); + } + } + + void _setNotCurrent() { + _isCurrentRow = false; + } +} + +Type _typeFromCode(int code) { + switch (code) { + case Types.SQLITE_INTEGER: + return Type.Integer; + case Types.SQLITE_FLOAT: + return Type.Float; + case Types.SQLITE_TEXT: + return Type.Text; + case Types.SQLITE_BLOB: + return Type.Blob; + case Types.SQLITE_NULL: + return Type.Null; + } + throw Exception("Unknown type [$code]"); +} + +Type _typeFromText(String textRepresentation) { + switch (textRepresentation) { + case "integer": + return Type.Integer; + case "float": + return Type.Float; + case "text": + return Type.Text; + case "blob": + return Type.Blob; + case "null": + return Type.Null; + } + if (textRepresentation == null) return Type.Null; + throw Exception("Unknown type [$textRepresentation]"); +} + +enum Type { Integer, Float, Text, Blob, Null } + +enum Convert { DynamicType, StaticType } + +class SQLiteException { + final String message; + SQLiteException(this.message); + + String toString() => message; +} diff --git a/samples/ffi/sqlite/lib/src/ffi/arena.dart b/samples/ffi/sqlite/lib/src/ffi/arena.dart new file mode 100644 index 00000000000..116a2e57821 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/ffi/arena.dart @@ -0,0 +1,53 @@ +import "dart:async"; +import "dart:ffi"; + +/// [Arena] manages allocated C memory. +/// +/// Arenas are zoned. +class Arena { + Arena(); + + List> _allocations = []; + + /// Bound the lifetime of [ptr] to this [Arena]. + T scoped(T ptr) { + _allocations.add(ptr.cast()); + return ptr; + } + + /// Frees all memory pointed to by [Pointer]s in this arena. + void finalize() { + for (final ptr in _allocations) { + ptr.free(); + } + } + + /// The last [Arena] in the zone. + factory Arena.current() { + return Zone.current[#_currentArena]; + } +} + +/// Bound the lifetime of [ptr] to the current [Arena]. +T scoped(T ptr) => Arena.current().scoped(ptr); + +class RethrownError { + dynamic original; + StackTrace originalStackTrace; + RethrownError(this.original, this.originalStackTrace); + toString() => """RethrownError(${original}) +${originalStackTrace}"""; +} + +/// Runs the [body] in an [Arena] freeing all memory which is [scoped] during +/// execution of [body] at the end of the execution. +R runArena(R Function(Arena) body) { + Arena arena = Arena(); + try { + return runZoned(() => body(arena), + zoneValues: {#_currentArena: arena}, + onError: (error, st) => throw RethrownError(error, st)); + } finally { + arena.finalize(); + } +} diff --git a/samples/ffi/sqlite/lib/src/ffi/cstring.dart b/samples/ffi/sqlite/lib/src/ffi/cstring.dart new file mode 100644 index 00000000000..e7b243b187d --- /dev/null +++ b/samples/ffi/sqlite/lib/src/ffi/cstring.dart @@ -0,0 +1,39 @@ +import "dart:convert"; +import "dart:ffi"; + +import "arena.dart"; + +/// Represents a String in C memory, managed by an [Arena]. +class CString extends Pointer { + /// Allocates a [CString] in the current [Arena] and populates it with + /// [dartStr]. + factory CString(String dartStr) => CString.inArena(Arena.current(), dartStr); + + /// Allocates a [CString] in [arena] and populates it with [dartStr]. + factory CString.inArena(Arena arena, String dartStr) => + arena.scoped(CString.allocate(dartStr)); + + /// Allocate a [CString] not managed in and populates it with [dartStr]. + /// + /// This [CString] is not managed by an [Arena]. Please ensure to [free] the + /// memory manually! + factory CString.allocate(String dartStr) { + List units = Utf8Encoder().convert(dartStr); + Pointer str = allocate(count: units.length + 1); + for (int i = 0; i < units.length; ++i) { + str.elementAt(i).store(units[i]); + } + str.elementAt(units.length).store(0); + return str.cast(); + } + + /// Read the string for C memory into Dart. + static String fromUtf8(CString str) { + if (str == null) return null; + int len = 0; + while (str.elementAt(++len).load() != 0); + List units = List(len); + for (int i = 0; i < len; ++i) units[i] = str.elementAt(i).load(); + return Utf8Decoder().convert(units); + } +} diff --git a/samples/ffi/sqlite/lib/src/ffi/dylib_utils.dart b/samples/ffi/sqlite/lib/src/ffi/dylib_utils.dart new file mode 100644 index 00000000000..1c924d4a838 --- /dev/null +++ b/samples/ffi/sqlite/lib/src/ffi/dylib_utils.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:ffi' as ffi; +import 'dart:io' show Platform; + +String _platformPath(String name, {String path}) { + if (path == null) path = ""; + if (Platform.isLinux) return path + "lib" + name + ".so"; + if (Platform.isMacOS) return path + "lib" + name + ".dylib"; + if (Platform.isWindows) return path + name + ".dll"; + throw Exception("Platform not implemented"); +} + +ffi.DynamicLibrary dlopenPlatformSpecific(String name, {String path}) { + String fullPath = _platformPath(name, path: path); + return ffi.DynamicLibrary.open(fullPath); +} diff --git a/samples/ffi/sqlite/pubspec.yaml b/samples/ffi/sqlite/pubspec.yaml new file mode 100644 index 00000000000..8b0136cca36 --- /dev/null +++ b/samples/ffi/sqlite/pubspec.yaml @@ -0,0 +1,9 @@ +name: sqlite3 +version: 0.0.1 +description: >- + Sqlite3 wrapper. Demo for dart:ffi. +author: Daco Harkes , Samir Jindel +environment: + sdk: '>=2.1.0 <3.0.0' +dev_dependencies: + test: ^1.5.3 \ No newline at end of file diff --git a/samples/ffi/sqlite/test/sqlite_test.dart b/samples/ffi/sqlite/test/sqlite_test.dart new file mode 100644 index 00000000000..bd14f65f611 --- /dev/null +++ b/samples/ffi/sqlite/test/sqlite_test.dart @@ -0,0 +1,160 @@ +// VMOptions=--optimization-counter-threshold=5 + +import "package:test/test.dart"; + +import 'package:sqlite3/sqlite.dart'; + +void main() { + test("sqlite integration test", () { + Database d = Database("test.db"); + d.execute("drop table if exists Cookies;"); + d.execute(""" + create table Cookies ( + id integer primary key, + name text not null, + alternative_name text + );"""); + d.execute(""" + insert into Cookies (id, name, alternative_name) + values + (1,'Chocolade chip cookie', 'Chocolade cookie'), + (2,'Ginger cookie', null), + (3,'Cinnamon roll', null) + ;"""); + Result result = d.query(""" + select + id, + name, + alternative_name, + case + when id=1 then 'foo' + when id=2 then 42 + when id=3 then null + end as multi_typed_column + from Cookies + ;"""); + for (Row r in result) { + int id = r.readColumnAsInt("id"); + expect(true, 1 <= id && id <= 3); + String name = r.readColumnByIndex(1); + expect(true, name is String); + String alternativeName = r.readColumn("alternative_name"); + expect(true, alternativeName is String || alternativeName == null); + dynamic multiTypedValue = r.readColumn("multi_typed_column"); + expect( + true, + multiTypedValue == 42 || + multiTypedValue == 'foo' || + multiTypedValue == null); + print("$id $name $alternativeName $multiTypedValue"); + } + result = d.query(""" + select + id, + name, + alternative_name, + case + when id=1 then 'foo' + when id=2 then 42 + when id=3 then null + end as multi_typed_column + from Cookies + ;"""); + for (Row r in result) { + int id = r.readColumnAsInt("id"); + expect(true, 1 <= id && id <= 3); + String name = r.readColumnByIndex(1); + expect(true, name is String); + String alternativeName = r.readColumn("alternative_name"); + expect(true, alternativeName is String || alternativeName == null); + dynamic multiTypedValue = r.readColumn("multi_typed_column"); + expect( + true, + multiTypedValue == 42 || + multiTypedValue == 'foo' || + multiTypedValue == null); + print("$id $name $alternativeName $multiTypedValue"); + if (id == 2) { + result.close(); + break; + } + } + try { + result.iterator.moveNext(); + } on SQLiteException catch (e) { + print("expected exception on accessing result data after close: $e"); + } + try { + d.query(""" + select + id, + non_existing_column + from Cookies + ;"""); + } on SQLiteException catch (e) { + print("expected this query to fail: $e"); + } + d.execute("drop table Cookies;"); + d.close(); + }); + + test("concurrent db open and queries", () { + Database d = Database("test.db"); + Database d2 = Database("test.db"); + d.execute("drop table if exists Cookies;"); + d.execute(""" + create table Cookies ( + id integer primary key, + name text not null, + alternative_name text + );"""); + d.execute(""" + insert into Cookies (id, name, alternative_name) + values + (1,'Chocolade chip cookie', 'Chocolade cookie'), + (2,'Ginger cookie', null), + (3,'Cinnamon roll', null) + ;"""); + Result r = d.query("select * from Cookies;"); + Result r2 = d2.query("select * from Cookies;"); + r.iterator..moveNext(); + r2.iterator..moveNext(); + r.iterator..moveNext(); + Result r3 = d2.query("select * from Cookies;"); + r3.iterator..moveNext(); + expect(2, r.iterator.current.readColumn("id")); + expect(1, r2.iterator.current.readColumn("id")); + expect(1, r3.iterator.current.readColumn("id")); + r.close(); + r2.close(); + r3.close(); + d.close(); + d2.close(); + }); + + test("stress test", () { + Database d = Database("test.db"); + d.execute("drop table if exists Cookies;"); + d.execute(""" + create table Cookies ( + id integer primary key, + name text not null, + alternative_name text + );"""); + int repeats = 100; + for (int i = 0; i < repeats; i++) { + d.execute(""" + insert into Cookies (name, alternative_name) + values + ('Chocolade chip cookie', 'Chocolade cookie'), + ('Ginger cookie', null), + ('Cinnamon roll', null) + ;"""); + } + Result r = d.query("select count(*) from Cookies;"); + int count = r.first.readColumnByIndexAsInt(0); + expect(count, 3 * repeats); + r.close(); + d.close(); + }); +}