diff --git a/samples/ffi/sqlite/.gitignore b/samples/ffi/sqlite/.gitignore
new file mode 100644
index 0000000000000..7a6bc2eeab30d
--- /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 0000000000000..6659300cf8f8f
--- /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 0000000000000..6ffa8a340a58c
--- /dev/null
+++ b/samples/ffi/sqlite/docs/lib/scenario-default.svg
@@ -0,0 +1,130 @@
+
+
+
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 0000000000000..4ae18c501a832
--- /dev/null
+++ b/samples/ffi/sqlite/docs/lib/scenario-full.svg
@@ -0,0 +1,149 @@
+
+
+
diff --git a/samples/ffi/sqlite/docs/sqlite-tutorial.md b/samples/ffi/sqlite/docs/sqlite-tutorial.md
new file mode 100644
index 0000000000000..7f7aa22a30b3a
--- /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 0000000000000..ae7bdfbda02fa
--- /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 0000000000000..e92743a4b3743
--- /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 0000000000000..fbcf922350515
--- /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 0000000000000..06de8383282df
--- /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 0000000000000..9cb709c6ef9d9
--- /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:
+///
+///
+///
Create the object using [sqlite3_prepare_v2()] or a related
+/// function.
+///
Bind values to [host parameters] using the sqlite3_bind_*()
+/// interfaces.
+///
Run the SQL by calling [sqlite3_step()] one or more times.
+///
Reset the statement using [sqlite3_reset()] then go back
+/// to step 2. Do this zero or more times.
+///
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 0000000000000..d5a052325749e
--- /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 0000000000000..11c214086192a
--- /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 0000000000000..116a2e5782119
--- /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 0000000000000..e7b243b187d58
--- /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 0000000000000..1c924d4a838d1
--- /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 0000000000000..8b0136cca366f
--- /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 0000000000000..bd14f65f611f6
--- /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();
+ });
+}