| # Dart Dynamic Modules | |
| ## Quick Disclaimers | |
| *Dart Dynamic Modules* is a work-in-progress and experimental. Like | |
| any experimental Dart feature, that means it might change in | |
| breaking ways and we are not yet committed to adopting it. Please be | |
| aware of the risks if you consider using it beyond local | |
| experiments. | |
| At this time, we are prioritizing work for Dynamic Modules and Dart | |
| Bytecodes that helps us improve specific developer journeys. We have | |
| focused on collaborative sharing of prototypes, and mobile | |
| development without JITing. We are not currently prioritizing work | |
| related to other applications of Dynamic Modules in production | |
| applications, like server-driven UI use-cases. | |
| Regardless of the use case, this feature needs to be treated with | |
| responsibility and care. Further | |
| [below](##technical-details--considerations--faq) we share many | |
| considerations, from security to policy, that you should be aware of | |
| before proceeding. | |
| ## What are Dart Dynamic Modules? | |
| Dart Dynamic Modules allows delivering code to an existing | |
| application and loading it dynamically much like a dynamic import in | |
| JavaScript or a dynamically linked library in C. | |
| We'll dive into two parts in a bit more detail: | |
| * **Semantics**: Until now Dart primarily supported statically | |
| compiled applications. This feature provides semantic reasoning | |
| for how to expand applications dynamically, mechanisms to declare | |
| these expansions, and validation needed to ensure these | |
| expansions don't break memory safety. | |
| * **Technology**: How we implemented it for AOT applications by | |
| using Dart bytecodes to deliver dynamic modules. | |
| Even though we don't have implementations for Dart web backends | |
| today, we have explored prototypes enough to convince ourselves that | |
| the semantics can be achieved using different technologies that fit | |
| naturally with those backends as well. | |
| ## Semantics | |
| Dart Dynamic Modules makes it possible to dynamically load a set of | |
| Dart libraries in a running Dart application. It defines how those | |
| libraries can interact with the rest of the application, and | |
| provides utilities to facilitate their construction and correct | |
| usage. | |
| ### Basic architecture: Leaving a closed-world behind | |
| Historically, we have mostly treated Dart applications as a | |
| monolith. Every time we build a Dart binary, we typically build the | |
| entire application as a unit. Yes, there are some exceptions in | |
| development scenarios where we support modular compilation, like DDC | |
| on the web, but this whole-program approach is generally true across | |
| our backends (dart2wasm, dart2js, AOT). Even features like deferred | |
| loading that allow developers to split their binary into pieces to | |
| lower latency during an initial load, operate with the whole program | |
| first and only later they split the code into chunks. | |
| Dart Dynamic Modules moves us away from this monolith concept. Now, | |
| an application can be built and more modules can be defined, | |
| compiled, and loaded after the fact. This represents architectural | |
| change: the application now acts as a ***host*** that is extensible | |
| through dynamic modules over time. The main semantic restriction: | |
| all extensions are additive. Dynamic modules cannot replace an | |
| existing declaration in the application, not even if that | |
| declaration was delivered through a dynamic module earlier. | |
| To extend an application, developers are required to define an API | |
| surface area that establishes how dynamic modules will interact with | |
| the rest of the host application. We call the declaration of this | |
| surface area a ***Dynamic Interface***. With a surface area defined, | |
| a host application is built as a single binary like before, but with | |
| the caveat that the API surface area is now exposed. More libraries | |
| can be compiled later on and loaded as dynamic modules, as long as | |
| they are compatible with the API surface area defined by the host. | |
| Quick recap on our terminology: | |
| * **Host application**: the application that will load the dynamic | |
| module. | |
| * **Dynamic interface**: a declaration of the API surface area | |
| exposed by a host application and made accessible to a dynamic | |
| module. | |
| * **Dynamic module**: a set of Dart libraries and an entrypoint | |
| method, that are loaded dynamically. | |
| ### Typical steps to use dynamic modules | |
| Let's dive into more details on how this architecture plays out. If | |
| you are interested in how to use them directly and experiment with | |
| them, skip ahead and look at our [example](#example). | |
| Here we describe the conceptual steps that are needed when adopting | |
| an application architecture that uses dynamic modules: | |
| * Step 1: First, you will need to have a special build of Dart that | |
| supports dynamic modules. We don't ship this as part of the Dart | |
| or Flutter SDKs, so at this time this needs to be built from | |
| sources (we show how in the [example below](#example)). | |
| * Step 2: You need to determine an API surface area. To do so, you | |
| usually start by imagining what logic will be loaded dynamically | |
| and how it will be integrated into your application. This helps | |
| you figure out the right contract between the application and the | |
| dynamic modules, when and how the application will load them, and | |
| identify which APIs need to be exposed. By default, nothing is | |
| exposed, not even APIs in `dart:core`, unless you opt into them. | |
| * Step 3: Once you decide which APIs you want to expose from the | |
| host, you write a special `dynamic_interface.yaml` file that | |
| defines the dynamic interface (see | |
| [format specification][dynamic_interface.md]). | |
| This file is later used as an input to all compilation steps. | |
| * Step 4: With a dynamic interface in hand, you can build the host | |
| application. There are three differences from how normal Dart | |
| applications are built: | |
| * First, the compiler and runtime are slightly different in order | |
| to include support for executing the dynamic modules. As we'll | |
| see later below, for AOT we decided to adopt Dart bytecodes and | |
| embed an interpreter in the runtime as the technology to execute | |
| dynamic modules in AOT. | |
| * Second, the application code is compiled differently by | |
| informing the compiler about the dynamic interface. | |
| Traditionally, Dart's AOT compiler performs closed-world | |
| optimizations assuming all code is known to the compiler, | |
| dynamic modules break that assumption so the compiler needs | |
| guidance to produce correct and sound applications. Usually this | |
| means less code is tree-shaken and fewer calls can be | |
| specialized because we don't know how exposed APIs will be used. | |
| * Third, the application code needs to embed some ***provenance*** | |
| information that will be used later to validate the | |
| compatibility of what we load dynamically. This is important to | |
| guarantee correctness and memory safety and is a consequence of | |
| the technological choices we made for implementing dynamic | |
| modules. (Note: provenance is work in progress and not available | |
| yet in the current implementation of the package). | |
| * Step 5: At this stage the application is ready to be delivered and | |
| run. The logic of what comes in a dynamic module can be defined | |
| later, even after the application is running. | |
| * Step 6: Create and build a dynamic module. This involves writing a | |
| new set of Dart libraries with a special entrypoint. There is an | |
| important constraint, dynamic modules can only define new code. If | |
| they define libraries that already exist in the host application | |
| or in previous dynamic modules, the module is going to be rejected | |
| during the load. To deliver a dynamic module, we first compile it | |
| to a format compatible with the runtime where the application is | |
| running. In the case of AOT, we use a bytecode compiler to produce | |
| the dynamic module. As hinted above, bytecode compilation not only | |
| produces the bytecode that we will run, but also provenance | |
| attestations needed in the validation process. | |
| * Step 7: Deliver the dynamic module to the app. Since dynamic | |
| modules are delivered dynamically, some effort is needed to set up | |
| the right way to deliver the dynamic module properly and securely. | |
| This often involves some form of transport-level security (TLS). | |
| * Step 8: Validate and run the dynamic module. Before running the | |
| dynamic module — which mainly consists of invoking the designated | |
| entry point of the dynamic module and returning the result to the | |
| host application — we need assurances that the dynamic module | |
| won’t break the integrity of the application. Without validation, | |
| loading a dynamic module could break the application in unexpected | |
| ways (e.g. crashes, heap corruption, and more). Validation | |
| typically confirms compatibility of the dynamic module format and | |
| APIs in use. Depending on the technology, the verification may be | |
| done differently. Our implementation based on bytecodes relies on | |
| provenance checks to provide validation. Dynamic modules are | |
| allowed to load only if the same application code, | |
| dynamic-interface, and version of the SDK tools were used to build | |
| both the host app and the dynamic module. | |
| That's the basic principle. | |
| ### Additional semantic details | |
| This isn't a complete or formal spec, but here are some details to | |
| expect about dynamic modules: | |
| * It's a static error to tear off or invoke a member that is not | |
| exposed as "callable" in the dynamic interface. This rejection | |
| typically happens in Step 6 of our conceptual steps above. | |
| * It's a static error to extend a class that was not exposed as | |
| "extendable" in the dynamic interface. | |
| * It's a static error to override a member that was not exposed as | |
| "can-be-overridden" in the dynamic interface. | |
| * It's a static error to use a class as a type that was not exposed | |
| as "callable" or "can-be-used-as-type" in the dynamic interface. | |
| * All direct and interface calls are validated statically. | |
| * Dynamic calls are generally discouraged because they can't be | |
| verified statically. They are, however, supported with special | |
| rules: | |
| * By default, compilers will reject the use of a dynamic call | |
| unless the selector name used in the dynamic call was previously | |
| allowed. | |
| * A selector name can be allowed if elements with the same name | |
| are exposed as "dynamically-callable" in the dynamic interface. | |
| This makes it possible to use dynamic calls that originate in | |
| dynamic modules but target a member in the host application. | |
| * A selector name can also be allowed through an allow-list flag | |
| of the dynamic module compiler. This makes it possible to use | |
| dynamic calls that originate from dynamic modules and target | |
| members declared also in dynamic modules. | |
| * Unlike all other parts of the API surface area, runtime checks | |
| are performed to enforce that the dynamic interface was | |
| respected. Runtime errors may be reported on dynamic calls that | |
| originate in the dynamic module if the target of the call was a | |
| member of the host application that was not exposed as | |
| "dynamically-callable". | |
| * It's a runtime error to load a dynamic module with an incompatible | |
| format. | |
| * It's a runtime error to load a dynamic module that attempts to | |
| define a library with an import-URI that already exists in the | |
| program, regardless of whether that import-URI was used for a | |
| library defined by the host application or a prior dynamic module. | |
| ## Technology: Bytecode-based Implementation in AOT | |
| For each backend, there are many possible ways to bring dynamic | |
| modules to life. DDC, for instance, is a modular compiler, so a | |
| dynamic module can directly be implemented as a new module with very | |
| little change to the compiler architecture. For AOT, we decided to | |
| adopt an architecture that uses an interpreter. | |
| The Dart bytecode [interpreter][interpreter.cc] exists in the Dart | |
| VM codebase. It requires a tight integration with the runtime, which | |
| makes it difficult to deliver separately from the Dart runtime | |
| itself. At the same time, it has an implicit code-size cost on the | |
| runtime that we don't want to include by default on all Dart | |
| applications today. Because of these reasons, we don't ship it by | |
| default as part of our SDK today. Instead, to use the dynamic | |
| modules experiment, we require passing a build-time flag when | |
| building Dart. | |
| We also have a [dart2bytecode compiler][dart2bytecode], which we | |
| use to compile dynamic modules to bytecode. In the future, we expect | |
| the format to deliver dynamic modules will be expanded beyond | |
| bytecode to allow embedding resources (e.g. icons, images, | |
| provenance). | |
| Finally, we are in the process of putting together tools to produce | |
| provenance information. This includes information about how an app | |
| and dynamic module were created, including versioning details about | |
| the inputs and tools used in the process. This may be embedded from | |
| the bytecode compiler or provided separately. We'll update this | |
| README as we make progress on the implementation. | |
| The libraries in `package:dynamic_modules` currently provide an API | |
| to load bytecode directly. This will be updated in the near future | |
| once we update the format and embed provenance information. Stay | |
| tuned for changes. | |
| ## Example | |
| The [`example/`](example/) directory contains a simple example that | |
| illustrates the steps described above. The example is a simple | |
| calculator command line interface that lets users extend it with new | |
| operations beyond those built-in. | |
| The example [README.md](example/README.md) contains details on how | |
| to navigate it. Here, we'd like to highlight how the example | |
| connects with some of the conceptual steps we described above. | |
| The calculator consists of 3 parts: | |
| * **An API surface area** exposes the interface used by dynamic | |
| modules to define new operations, as well as the API to register | |
| them. This corresponds to steps 2 and 3 above. See | |
| [`dynamic_interface.yaml`](example/dynamic_interface.yaml) | |
| and [`common/common.dart`](example/common/common.dart). | |
| * **The host app** defines the main REPL (read–eval–print loop) and | |
| provides one built-in operation (math addition). This corresponds | |
| to step 4 above. See [`host/main.dart`](example/host/main.dart). | |
| * **Dynamic modules** define new operations for the calculator | |
| following the API surface area defined by the host application. | |
| This corresponds to step 6 above. See the | |
| [`modules/`](example/modules/) subfolder. | |
| Finally everything comes together in the [`run.sh`](example/run.sh) | |
| script. This script shows: | |
| * How to build Dart SDK artifacts with support for Dart Dynamic | |
| Modules (Step 1 above) by providing the `--dart-dynamic-modules` | |
| build-time flag. | |
| * How to use the dynamic interface to build artifacts of steps 4 and | |
| 6. | |
| This example doesn't show how dynamic modules are delivered over the | |
| network (step 7), instead bytecode files are copied over to a data | |
| directory as an over simplistic way to represent the delivery | |
| process. | |
| ## Progress/Status | |
| Dynamic modules are designed so they can eventually be supported in | |
| any Dart backend, however we have primarily focused on the AOT | |
| implementation. All other backends are either unsupported or | |
| incomplete. | |
| What works? | |
| * AOT compilation factors dynamic-interface to prevent tree-shaking | |
| and invalid optimizations and to perform runtime validation of | |
| dynamic calls. | |
| * Modular validation of API usage against dynamic-interface. | |
| * Integration of bytecode interpreter in the AOT runtime. | |
| * Integration of bytecode interpreter in the JIT runtime. | |
| * Required runtime checks in the AOT runtime needed to validate | |
| dynamic calls emanating from dynamic modules. For context, most | |
| validation of dynamic modules is done statically and modularly | |
| while compiling dynamic modules. However, the use of dynamic calls | |
| in dynamic modules require an additional whole-program static | |
| check and runtime checks. | |
| What doesn't work yet? | |
| * Bytecode provenance validation. There is work in progress, but the | |
| APIs have not been migrated over. This is a known upcoming | |
| breaking change to our APIs. | |
| * Whole-program validation of dynamic modules. As mentioned earlier, | |
| dynamic calls also need a whole-program static check. This check | |
| is not implemented yet. | |
| * Required runtime checks in the JIT runtime to validate dynamic | |
| calls. This is missing mainly because the JIT runtime doesn't load | |
| the dynamic-interface when running a host application from | |
| sources. | |
| * Simple developer experience when authoring dynamic interfaces. | |
| Currently the `dynamic_interface.yaml` format has quirks | |
| especially around private members, which sometimes need to be | |
| exposed due to low-level implementation details of the language. | |
| * Invoking FFI APIs from a dynamic module. | |
| What is currently not planned? | |
| * We are primarily focused on the AOT pipeline. We may improve | |
| support in JIT, but do not plan to focus on the web backends (DDC, | |
| dart2js, or dart2wasm) at this time. While this package includes | |
| tests that use an old DDC implementation based on DDC's JS module | |
| systems, that implementation was primarily created to validate the | |
| dynamic-modules concepts and ensure the semantics were flexible | |
| enough to support Dart backends other than AOT. | |
| ## Technical details, Considerations, FAQ | |
| **Q: How are dynamic modules executed in AOT?** | |
| The current implementation in AOT focuses entirely on using bytecode | |
| to deliver dynamic modules and executing it using a bytecode | |
| interpreter. That said, in general Dart Dynamic Modules as a concept | |
| is agnostic of how code gets executed. We only define how code | |
| extends an existing application and how it interacts with it. In | |
| fact, prototypes we have built with DDC and dart2wasm leveraged | |
| their own JS or wasm module systems to load new code. | |
| **Q: Are dynamic modules fully sandboxed?** | |
| No. Dynamic modules run in an interpreter, but you are loading code | |
| into the same context of the application. Both the original host | |
| application code and the dynamic modules run together as a single | |
| program with a shared heap. Even though dynamic modules have | |
| restricted APIs to refer to, there are no restrictions on what those | |
| APIs can access indirectly. | |
| You can create some lightweight isolation by loading dynamic modules | |
| in a new Dart isolate. This separates the heap from the rest of the | |
| host application, but won’t provide a full sandbox as it will still | |
| be running in the same OS process as the app. | |
| We think it is an interesting future direction to explore creating | |
| trim-down versions of the Dart runtime that are fully sandboxed | |
| (e.g. absolute no access to IO or FFI), though. | |
| **Q: Is it OK to load untrusted/external/third-party code as a | |
| dynamic module?** | |
| No. As mentioned above, there is no sandbox. Anything you load with | |
| a dynamic module requires a lot of trust. Just like it's not OK to | |
| load untrusted third-party code without a sandbox in the same | |
| context as your web application, it's not OK to do so in a Dart | |
| application. Any code that is not developed by your team (external | |
| contributions, unreviewed LLM generated code, etc) should be treated | |
| as untrusted. | |
| **Q: Why will the API change to require provenance information?** | |
| We will require provenance information to validate compatibility | |
| before a dynamic module is loaded and run in a host application. | |
| Unlike other bytecode formats, like Java bytecodes, the Dart | |
| bytecode format has no stable ABI and no mechanism to verify that | |
| APIs used from the bytecode are compatible. As such, we need | |
| additional mechanisms to prove compatibility and verify that a | |
| dynamic module will behave as expected when loaded into a running | |
| host application. Since compilation to bytecode already performs | |
| most of the verification steps we need, we are designing the system | |
| to plumb provenance information as a way to give the host | |
| application the proof it needs to confirm everything is in order | |
| before proceeding. | |
| **Q: Why are Dart bytecodes and Dart dynamic modules being | |
| developed?** | |
| We are currently exploring improvements to the development story of | |
| Dart and Flutter applications. Historically we relied heavily on JIT | |
| compilation, but we were exploring using modular AOT compilation in | |
| combination with Dart bytecodes to provide alternative development | |
| workflows that can be supported in environments where JIT | |
| compilation is not feasible. We discovered that the same technology | |
| can be used for loading code dynamically, which led us to explore | |
| new development use cases of the technology. One such use-case is to | |
| share prototypes between team members at a low cost. | |
| **Q: Can I use dynamic modules today in my Dart/Flutter app?** | |
| We are in an early experimental stage of this feature, so we don't | |
| recommend taking a dependency on it for projects used in production | |
| today. You'll note that the feature is also not available by default | |
| in Dart and Flutter SDKs, in part because just enabling it | |
| introduces code-size bloat and performance effects that are | |
| unnecessary for existing applications today. | |
| We are prioritizing dynamic modules for development use cases, where | |
| enabling dynamic modules at build time is an option and the | |
| trade-offs are less of an issue. | |
| **Q: Why are you only prioritizing development use cases?** | |
| There are many considerations that need to be factored when | |
| supporting a feature for production use cases. In the case of Dart | |
| Dynamic Modules, the consideration list is long and requires a lot | |
| of diligence, care, and effort, both from the SDK and from actual | |
| developers adopting it. We decided to focus only on development use | |
| cases that allow us to reduce the scope of these considerations. | |
| Here are some of the top-of-mind factors for this technology: | |
| * This feature can increase application size unnecessarily. The | |
| interpreter has a fixed cost, but there are additional effects on | |
| AOT binaries from disabling tree-shaking and optimizations related | |
| to the dynamic interface. If used naively, that extra size bloat | |
| can be substantial. | |
| * If used improperly, this feature can introduce security risks. | |
| Dynamic modules are loaded in a trusted environment with no extra | |
| sandbox, so care needs to be in place to ensure bytecode has the | |
| right provenance and is delivered safely. Our work on requiring | |
| provenance is an effort to reduce this risk, but it doesn't | |
| eliminate all attack vectors. | |
| * If used improperly, this feature can break applications or cause | |
| them to behave unexpectedly. For example, accidentally mixing | |
| bytecode compiled against a different version of the app can cause | |
| issues. Our work on provenance also helps reduce this risk by | |
| validating compatibility, but it doesn't reduce the complexity | |
| required to manage versioning in the first place. | |
| * Proper version management involves non-trivial efforts, including | |
| creating server components and properly managing creation and | |
| storage of bytecode. We think a production-ready solution should | |
| have clear guidance and designs for how to address this challenge. | |
| By focusing on the development use cases we found that some of these | |
| considerations have a lower impact or lower complexity. For | |
| instance, code-size is a lesser concern and we can focus on | |
| single-version scenarios that ignore the more general versioning | |
| questions. We still believe these considerations are important and | |
| relevant, so we have used the development scenarios to help us make | |
| progress on them (e.g. provenance support). | |
| **Q: Is Dart Dynamic Modules a "code-push" implementation?** | |
| No. Dart Dynamic Modules is a language runtime technology, similar | |
| to that available in many other languages. It resembles C dynamic | |
| linking, JavaScript dynamic imports, wasm module instantiations. | |
| Code-push approaches are full-stack solutions that use multiple | |
| technologies and services underneath to enable developers to send | |
| over-the-air updates that patch existing applications. A general | |
| code-push solution needs hosting services to track releases and | |
| patches over time, mechanisms to make the application updatable | |
| in-place, to determine what has changed between patches, to encode | |
| updates in a deliverable format, to deliver and apply such updates, | |
| and more. | |
| We believe Dart Dynamic Modules is not an ideal technology for a | |
| general-purpose codepush solution for a few reasons. First, the | |
| additive semantics: dynamic modules can only introduce new | |
| libraries, but can't update any existing declaration in an | |
| application. Second, the API surface area: dynamic modules can only | |
| invoke code that was previously exposed. In contrast, code-push | |
| solutions want to update existing declarations and want patches to | |
| be able to invoke anything already in the application. For a dynamic | |
| module to be able to invoke anything in an application, we would | |
| need to expose the entire app in the dynamic interface. That would | |
| practically disable tree-shaking and all whole-program | |
| optimizations, dramatically affecting the original application | |
| performance and code-size to the point that we doubt it would be | |
| suitable for use in production environments. | |
| **Q: Are there security concerns behind dynamic modules?** | |
| Yes. As hinted above in the earlier questions about how it works, | |
| some top security considerations are that: | |
| * A dynamic module loads in the same context as the host app, | |
| without an additional sandbox. Compared to the web, this is | |
| equivalent to appending `<script>` tags without using an | |
| iframe. As such, similar precautions are needed to use dynamic | |
| modules safely. This includes ensuring the code is trusted, | |
| preventing injection scenarios that would tamper the integrity of | |
| the bytecode. For example, you should never use dynamic modules to | |
| load code provided by others or provided by LLMs. Also, even if | |
| the source of the code is trusted, the delivery needs to be | |
| secured with proper TLS. | |
| * Our bytecode doesn't have a stable ABI, so compatibility is only | |
| guaranteed if the bytecode loaded matches the format supported by | |
| the host app. A skew in the ABI can cause memory corruption or | |
| other semantic errors and lead the application to misbehave. | |
| * Similarly, incompatible APIs can also lead to the same result. | |
| Our work to provide provenance validation is designed to help | |
| validate ABI and API compatibility before a load is allowed. Unlike | |
| Java bytecodes, Dart bytecodes don't offer verification that the | |
| APIs in the bytecode match the APIs in the host app, so we resort to | |
| provenance as an alternative technique. Just like the web, | |
| provenance alone does not guarantee the safety of the code being | |
| loaded. Special diligence is required by developers to ensure that | |
| only code that is trusted and delivered securely is ever loaded in | |
| the context of the application. | |
| **Q: Are there store policy considerations to consider when using | |
| dynamic modules?** | |
| Yes, there are policy considerations, regardless of how code is run. | |
| For example, you might want to validate that any store you are | |
| publishing to allows running interpreted code in a custom | |
| interpreter. And stores might have policies around how much an app | |
| is allowed to change after it has been installed by the user. | |
| [interpreter.cc]: https://github.com/dart-lang/sdk/blob/898a1e4bbfbc472dc0a9505dc7d2e4c21d6f856e/runtime/vm/interpreter.cc | |
| [dart2bytecode]: https://github.com/dart-lang/sdk/blob/898a1e4bbfbc472dc0a9505dc7d2e4c21d6f856e/pkg/dart2bytecode/ | |
| [dynamic_interface.md]: https://github.com/dart-lang/sdk/blob/898a1e4bbfbc472dc0a9505dc7d2e4c21d6f856e/pkg/vm/dynamic_interface.md |