| # 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 |