├── .formatter.exs ├── .gitignore ├── .tool-versions ├── README.md ├── assets ├── dart-example.png └── elixir-example-handler.png ├── icudtl.dat ├── lib ├── compile.flutter.ex ├── flutter_embedder.ex ├── flutter_embedder │ ├── mdns_client.ex │ ├── platform_channel_message.ex │ ├── standard_message_codec.ex │ ├── standard_method_call.ex │ ├── standard_method_call │ │ └── handler.ex │ └── stub_handler.ex └── mix.tasks.flutter.discover.ex ├── mix.exs ├── mix.lock └── src ├── Makefile ├── debug.h ├── embedder.c ├── embedder_drm.c ├── embedder_gfx.h ├── embedder_glfw.c ├── embedder_platform_message.c ├── embedder_platform_message.h ├── erlcmd.c ├── erlcmd.h ├── precompiled_engine_aarch64.mk ├── precompiled_engine_armv7.mk └── precompiled_engine_x86_64.mk /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | flutter_embedder-*.tar 24 | .dart_tool 25 | .vscode 26 | build 27 | flutter_embedder.h 28 | *.zip 29 | *.orig 30 | *.txt 31 | /sample -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | flutter 1.20.2-stable 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlutterEmbedder 2 | 3 | [Flutter](https://flutter.dev) is an open-source UI software development kit 4 | created by Google. This project aims to use the Flutter "embedder" system to 5 | provide embedded UI applications targeting Nerves devices. 6 | 7 | ## Similar Projects 8 | 9 | This idea isn't completely original. Some notable similar projects include: 10 | 11 | * [flutter-pi](https://github.com/ardera/flutter-pi) - A light-weight Flutter Engine Embedder for Raspberry Pi that runs without X. 12 | * [go-flutter](https://github.com/go-flutter-desktop/go-flutter) - Flutter on Windows, MacOS and Linux - based on Flutter Embedding, Go and GLFW. 13 | * [Flutter from Scratch](https://medium.com/flutter/flutter-on-raspberry-pi-mostly-from-scratch-2824c5e7dcb1) - A really nice introduction to the Flutter Embedder. 14 | 15 | ## Getting Started 16 | 17 | > NOTE: this is still very much a work in progress project and document. 18 | 19 | The below document assumes you use something akin to [Poncho Projects](https://hexdocs.pm/nerves/1.3.2/user-interfaces.html#choosing-a-project-structure) 20 | when developing your Nerves application. This guide will take you through 21 | creating both the `nerves` and `flutter` application codebases, but adding 22 | `flutter` to an existing app should be equally as easy. 23 | 24 | ### Dependencies and setup 25 | 26 | The first steps will be to install Nerves and Flutter. You can find guides to 27 | both below: 28 | 29 | * [Nerves](https://hexdocs.pm/nerves/1.3.2/installation.html#content) 30 | * [Flutter](https://flutter.dev/docs/get-started/install) 31 | 32 | > NOTE: Currently this project is only compatible with Flutter 1.22.4. 33 | 34 | ```bash 35 | flutter version 1.22.4 36 | ``` 37 | 38 | Once that's complete create a new folder for your project and then scafold two 39 | applications: 40 | 41 | ```bash 42 | mkdir flutter-nerves-helloworld 43 | cd flutter-nerves-helloworld 44 | # Create a new Nerves Project 45 | # currently, only rpi4 is supported. 46 | mix nerves.new firmware --target=rpi4 47 | # Create a new flutter application 48 | flutter create ui 49 | ``` 50 | 51 | ### Wiring it up 52 | 53 | The first step to getting up and running is to add `flutter_embedder` to your 54 | Elixir application's dependencies. Open `mix.exs`: 55 | 56 | ```elixir 57 | def deps do 58 | # ... ommited for clarity 59 | # Dependencies for specific targets 60 | # Add this line 61 | {:flutter_embedder, "~> 0.0", targets: :rpi4} 62 | end 63 | ``` 64 | 65 | Then run a quick `mix deps.get` in that folder. This will fetch the 66 | flutter_embedder project. 67 | 68 | Next, in that same file add a new option to the `def project` section: 69 | 70 | ```elixir 71 | def project do 72 | # ... ommited for clarity 73 | # add a new line to configure elixir to run the `flutter` compile step: 74 | compilers: compilers(Mix.target()), 75 | # Configure the flutter compiler to compile the app we created 76 | # in the previous step 77 | flutter: [ 78 | cd: Path.expand("../ui", __DIR__) 79 | ] 80 | end 81 | 82 | # Create this function: 83 | def compilers(:rpi4), do: [:flutter | Mix.compilers()] 84 | def compilers(:host), do: Mix.compilers() 85 | ``` 86 | 87 | The final step is to start the Flutter embedder in your application. The easiest 88 | way to do this is by adding the child to your supervision tree. Open 89 | `lib/application.ex`: 90 | 91 | ```elixir 92 | def children(_target) do 93 | flutter_opts = [ 94 | flutter_assets: Application.app_dir(:firmware, ["priv", "flutter_assets"]) 95 | ] 96 | [ 97 | # ... ommited for clarity 98 | {FlutterEmbedder, flutter_opts} 99 | ] 100 | end 101 | ``` 102 | 103 | > NOTE: By default Flutter uses `Ariel` font. These fonts require a license aggreement and are not 104 | supplied in this project. To boot flutter on your device, you will need this file in your `firmware` project: 105 | 106 | ```shell 107 | f11c0317db527bdd80fa0afa04703441 rootfs_overlay/usr/share/fonts/truetype/msttcorefonts/Arial.ttf 108 | ``` 109 | 110 | Finally, to get the application up and running, follow the standard Nerves 111 | workflow: 112 | 113 | ```bash 114 | mix firmware.burn 115 | ``` 116 | 117 | And when it boots, you should see the default flutter application. 118 | 119 | ### Dart Hot Code Reloading with Visual Studio Code 120 | 121 | One of the major selling points of Flutter is the ability to reload code instantly while developing. 122 | The Elixir Embedder supports this similarly to how [go-flutter](https://github.com/go-flutter-desktop/go-flutter) does it. 123 | 124 | In your `ui` project folder, create a file called `.vscode/launch.json` if it's not there already: 125 | 126 | ```json 127 | { 128 | // Use IntelliSense to learn about possible attributes. 129 | // Hover to view descriptions of existing attributes. 130 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 131 | "version": "0.2.0", 132 | "configurations": [ 133 | { 134 | "name": "Nerves Flutter Embedder", 135 | "request": "attach", 136 | "deviceId": "flutter-tester", 137 | "observatoryUri": "${command:dart.promptForVmService}", 138 | "type": "dart", 139 | "program": "lib/main.dart" // Dart-Code v3.3.0 required 140 | } 141 | ] 142 | } 143 | ``` 144 | 145 | After that's available, from your `firmware` folder open a terminal and issue the command: 146 | 147 | ```bash 148 | mix flutter.discover 149 | ``` 150 | 151 | It should return a result that looks something like: 152 | 153 | ```shell 154 | Discovering devices via MDNS 155 | ============================================================= 156 | 157 | Found Flutter Observatory: 192.168.1.127 158 | tunnel: ssh -L 46603:localhost:46603 192.168.1.127 159 | url: http://localhost:46603/is1QgudddHQ=/ 160 | 161 | launch.json: {"deviceId":"flutter-tester","name":"Nerves Flutter (192.168.1.127)","observatoryUri":"http://localhost:46603/is1QgudddHQ=/\n","program":"lib/main.dart","request":"attach","type":"dart"} 162 | ============================================================= 163 | ``` 164 | 165 | Now, copy the value in the `tunnel` section, execute it. This should open an SSH session to your 166 | device. Next copy the `url` section into your clipboard. 167 | 168 | Finally back in the `ui` editor, press `F5` and when it prompts you for a URL, paste the `url` from 169 | your clipboard. This will automatically connect to a debug session on the device. Every save you make to 170 | the Dart code will automatically sync over to the device. 171 | 172 | > NOTE: It must be noted that the `port` and `path` of this URL will change every time the Dart applicaiton 173 | is restarted, for example, reboot, crash, firmware upgrade etc. This means you will need to rerun this mix 174 | task every time one of those events happen. 175 | -------------------------------------------------------------------------------- /assets/dart-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartrent/elixir_flutter_embedder/c6734b5e1fed44475da46137087dce867d130fb7/assets/dart-example.png -------------------------------------------------------------------------------- /assets/elixir-example-handler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartrent/elixir_flutter_embedder/c6734b5e1fed44475da46137087dce867d130fb7/assets/elixir-example-handler.png -------------------------------------------------------------------------------- /icudtl.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartrent/elixir_flutter_embedder/c6734b5e1fed44475da46137087dce867d130fb7/icudtl.dat -------------------------------------------------------------------------------- /lib/compile.flutter.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Compile.Flutter do 2 | @moduledoc """ 3 | Light wrapper around the `flutter build bundle` command. 4 | 5 | Usage: 6 | 7 | def project do 8 | [ 9 | compilers: Mix.compilers() ++ [:flutter], 10 | flutter: [ 11 | cd: Path.expand("../ui", __DIR__) 12 | ] 13 | ] 14 | end 15 | 16 | See h(FlutterEmbedder) or the README.md for more info 17 | 18 | ## Options 19 | 20 | cd: directory to change into before compiling 21 | """ 22 | 23 | use Mix.Task.Compiler 24 | 25 | @recursive true 26 | @shortdoc @moduledoc 27 | 28 | def clean() do 29 | flutter_config = Mix.Project.config()[:flutter] 30 | cwd = File.cwd!() 31 | 32 | if flutter_config[:cd] do 33 | raise "???" 34 | File.cd!(flutter_config[:cd]) 35 | end 36 | 37 | System.cmd("flutter", ["clean"], into: IO.stream(:stdio, :line), cd: flutter_config[:cd]) 38 | File.cd!(cwd) 39 | end 40 | 41 | def run(args) do 42 | _ = Mix.Project.get!() 43 | flutter_config = Mix.Project.config()[:flutter] 44 | 45 | app_dir = Mix.Project.app_path() 46 | priv_dir = Path.join([app_dir, "priv"]) 47 | 48 | flutter_assets = Path.join(priv_dir, "flutter_assets") 49 | unless File.dir?(flutter_assets), do: File.mkdir_p!(flutter_assets) 50 | 51 | manifest_file = Path.join(priv_dir, "flutter.manifest") 52 | manifest = read_manifest(manifest_file) 53 | cwd = File.cwd!() 54 | 55 | if flutter_config[:cd] do 56 | File.cd!(flutter_config[:cd]) 57 | end 58 | 59 | target_files = Path.wildcard("lib/**/*.dart") 60 | target_manifest = create_manifest(target_files) 61 | 62 | cond do 63 | "--force" in args -> 64 | recompile(flutter_config, priv_dir) 65 | 66 | # naive check for created/deleted files. 67 | Enum.count(target_manifest) == Enum.count(manifest) -> 68 | maybe_recompile(flutter_config, target_manifest, manifest, priv_dir) 69 | 70 | true -> 71 | recompile(flutter_config, priv_dir) 72 | end 73 | 74 | write_manifest(target_manifest, manifest_file) 75 | File.cd!(cwd) 76 | {:ok, []} 77 | end 78 | 79 | def recompile(flutter_config, priv_dir) do 80 | Mix.shell().info("Compiling Flutter assets") 81 | flutter_assets = Path.join(priv_dir, "flutter_assets") 82 | 83 | {_, 0} = 84 | System.cmd("flutter", ["build", "bundle"], 85 | into: IO.stream(:stdio, :line), 86 | cd: flutter_config[:cd] 87 | ) 88 | 89 | File.cp_r!("build/flutter_assets", flutter_assets) 90 | end 91 | 92 | def maybe_recompile(flutter_config, target_manifest, manifest, app_dir) do 93 | should_recompile? = 94 | Enum.reduce(0..Enum.count(target_manifest), false, fn 95 | _, true -> 96 | true 97 | 98 | index, false -> 99 | Enum.at(target_manifest, index) != Enum.at(manifest, index) 100 | end) 101 | 102 | if should_recompile?, do: recompile(flutter_config, app_dir) 103 | end 104 | 105 | def read_manifest(manifest_file) do 106 | unless File.exists?(manifest_file) do 107 | File.touch!(manifest_file) 108 | end 109 | 110 | File.read!(manifest_file) 111 | |> String.trim() 112 | |> String.split("\n") 113 | |> Enum.filter(&(String.length(&1) > 0)) 114 | |> Enum.map(&String.split(&1, " ")) 115 | |> Enum.map(fn [filename, hash] -> {filename, hash} end) 116 | end 117 | 118 | def write_manifest(target_manifest, manifest_file) do 119 | binary_manifest = 120 | Enum.map(target_manifest, fn {filename, hash} -> Enum.join([filename, hash], " ") end) 121 | |> Enum.join("\n") 122 | 123 | File.write!(manifest_file, binary_manifest) 124 | end 125 | 126 | # Example: [{"lib/main.dart", "FD7CC9C62AC0ED61B813F607D2368E49"}] 127 | def create_manifest(target_files) do 128 | Enum.map(target_files, fn filename -> 129 | {filename, File.read!(filename) |> :erlang.md5() |> Base.encode16()} 130 | end) 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/flutter_embedder.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder do 2 | # @moduledoc File.read!("README.md") 3 | alias FlutterEmbedder.{PlatformChannelMessage, StandardMessageCodec, StandardMethodCall} 4 | import StandardMessageCodec, only: [is_valid_dart_value: 1] 5 | defstruct [:port, :uri, :module] 6 | 7 | require Logger 8 | use GenServer, child_spec: false 9 | 10 | def child_spec(opts) do 11 | %{ 12 | id: __MODULE__, 13 | start: {__MODULE__, :start_link, [opts]}, 14 | type: :worker, 15 | restart: :permanent, 16 | shutdown: 500 17 | } 18 | end 19 | 20 | def start_link(args, opts \\ []) do 21 | GenServer.start_link(__MODULE__, args, opts) 22 | end 23 | 24 | @impl GenServer 25 | def init(args) do 26 | case sanity_check(args) do 27 | {:ok, args} -> 28 | Logger.info("#{port_executable()} #{Enum.join(args, " ")}") 29 | 30 | port = 31 | Port.open({:spawn_executable, port_executable()}, [ 32 | {:args, args}, 33 | :binary, 34 | :exit_status, 35 | {:packet, 4}, 36 | # :nouse_stdio, 37 | {:env, 38 | [{'LD_LIBRARY_PATH', to_charlist(Application.app_dir(:flutter_embedder, ["priv"]))}]} 39 | ]) 40 | Logger.info "#{inspect(Port.info(port))}" 41 | 42 | {:ok, %__MODULE__{port: port, module: FlutterEmbedder.StubMethodCallHandler}} 43 | end 44 | end 45 | 46 | @impl true 47 | def terminate(_, state) do 48 | remove_mdns_service(state) 49 | end 50 | 51 | @impl GenServer 52 | def handle_info({port, {:exit_status, status}}, %{port: port} = state) do 53 | {:stop, {:flutter_embedder_crash, status}, state} 54 | end 55 | 56 | def handle_info({port, {:data, <<1, _::32, log::binary>>}}, %{port: port} = state) do 57 | Logger.info(log) 58 | 59 | case log do 60 | "flutter: Observatory listening on " <> uri -> 61 | uri = URI.parse(String.trim(uri)) 62 | state = %{state | uri: uri} 63 | # add_mdns_service(state) 64 | {:noreply, state} 65 | 66 | _ -> 67 | {:noreply, state} 68 | end 69 | end 70 | 71 | def handle_info({port, {:data, <<0, data::binary>>}}, %{port: port} = state) do 72 | platform_channel_message = PlatformChannelMessage.decode(data) 73 | # Logger.info("#{inspect(platform_channel_message)}") 74 | 75 | case StandardMethodCall.decode(platform_channel_message) do 76 | {:ok, call} -> 77 | handle_standard_call(platform_channel_message, call, state) 78 | 79 | {:error, reason} -> 80 | Logger.error( 81 | "Could not decode #{platform_channel_message.channel} message as StandardMethodCall: #{ 82 | reason 83 | } (this is probably ok)" 84 | ) 85 | 86 | reply_bin = 87 | PlatformChannelMessage.encode_response(platform_channel_message, :not_implemented) 88 | 89 | true = Port.command(state.port, reply_bin) 90 | {:noreply, state} 91 | end 92 | end 93 | 94 | def handle_standard_call( 95 | %PlatformChannelMessage{channel: channel} = call, 96 | %StandardMethodCall{method: method, args: args}, 97 | state 98 | ) do 99 | case state.module.handle_std_call(channel, method, args) |> IO.inspect(label: "reply") do 100 | {:ok, value} when is_valid_dart_value(value) -> 101 | value_ = StandardMessageCodec.encode_value(value) 102 | 103 | reply_bin = PlatformChannelMessage.encode_response(call, {:ok, value_}) 104 | 105 | true = Port.command(state.port, reply_bin) 106 | 107 | {:error, code, message, value} -> 108 | code_ = StandardMessageCodec.encode_value(code) 109 | message_ = StandardMessageCodec.encode_value(message) 110 | value_ = StandardMessageCodec.encode_value(value) 111 | 112 | reply_bin = 113 | PlatformChannelMessage.encode_response(call, {:error, code_ <> message_ <> value_}) 114 | 115 | true = Port.command(state.port, reply_bin) 116 | 117 | :not_implemented -> 118 | reply_bin = PlatformChannelMessage.encode_response(call, :not_implemented) 119 | true = Port.command(state.port, reply_bin) 120 | end 121 | 122 | {:noreply, state} 123 | end 124 | 125 | # TODO Check for errors instead of raising 126 | @doc false 127 | def sanity_check(args) do 128 | flutter_assets = 129 | args[:flutter_assets] || raise ArgumentError, "`flutter_assets` is a required argument" 130 | 131 | true = "vm_snapshot_data" in File.ls!(flutter_assets) 132 | 133 | icudtl_file = 134 | args[:icudtl_file] || Application.app_dir(:flutter_embedder, ["priv", "icudtl.dat"]) 135 | 136 | {:ok, ["#{flutter_assets}", "#{icudtl_file}"]} 137 | end 138 | 139 | @doc false 140 | def port_executable() do 141 | hack = "/root/flutter_embedder" 142 | 143 | exe = 144 | if File.exists?(hack) do 145 | :ok = File.chmod(hack, 0o777) 146 | hack 147 | else 148 | Application.app_dir(:flutter_embedder, ["priv", "flutter_embedder"]) 149 | end 150 | 151 | Logger.info("Using #{exe} for flutter_embedder") 152 | exe 153 | end 154 | 155 | def add_mdns_service(%{uri: uri}) do 156 | services = [ 157 | %{ 158 | name: "Flutter Observatory", 159 | protocol: "dartobservatory", 160 | transport: "tcp", 161 | port: uri.port, 162 | txt_payload: [URI.encode_query(%{path: uri.path, port: uri.port})] 163 | } 164 | ] 165 | 166 | MdnsLite.add_mdns_services(services) 167 | end 168 | 169 | def remove_mdns_service(_state) do 170 | MdnsLite.remove_mdns_services("Flutter Observatory") 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /lib/flutter_embedder/mdns_client.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.MDNSClient do 2 | @moduledoc "Simple MDNS Client to discover networked Nerves devices" 3 | use GenServer 4 | require Logger 5 | @mdns_group {224, 0, 0, 251} 6 | @mdns_port 5353 7 | 8 | defmodule State do 9 | defstruct mdns_socket: nil, discovered: [] 10 | end 11 | 12 | @query_packet %DNS.Record{ 13 | header: %DNS.Header{}, 14 | qdlist: [] 15 | } 16 | 17 | def start_link(args) do 18 | GenServer.start_link(__MODULE__, args, name: __MODULE__) 19 | end 20 | 21 | def discover(pid) do 22 | query(pid, :ptr, '_dartobservatory._tcp.local') 23 | end 24 | 25 | def query(client, type, domain) do 26 | GenServer.call(client, {:query, type, domain}) 27 | end 28 | 29 | @impl GenServer 30 | def init(_args) do 31 | send(self(), :open_mdns) 32 | {:ok, %State{}} 33 | end 34 | 35 | @impl GenServer 36 | def handle_call({:query, type, query}, from, state) do 37 | send_query(state.mdns_socket, type, query) 38 | Process.send_after(self(), {:query_result, from}, 1500) 39 | {:noreply, state} 40 | end 41 | 42 | @impl GenServer 43 | def handle_info({:query_result, from}, state) do 44 | results = Enum.map(state.discovered, fn {_ref, data} -> data end) 45 | GenServer.reply(from, {:ok, results}) 46 | {:noreply, state} 47 | end 48 | 49 | def handle_info({:ttl, ref}, state) do 50 | discovered = 51 | Enum.reject(state.discovered, fn 52 | {^ref, _} -> true 53 | _ -> false 54 | end) 55 | 56 | {:noreply, %{state | discovered: discovered}} 57 | end 58 | 59 | def handle_info(:open_mdns, state) do 60 | udp_options = [ 61 | :binary, 62 | broadcast: true, 63 | active: true, 64 | ip: {0, 0, 0, 0}, 65 | ifaddr: {0, 0, 0, 0}, 66 | add_membership: {@mdns_group, {0, 0, 0, 0}}, 67 | multicast_loop: true, 68 | multicast_ttl: 32, 69 | reuseaddr: true 70 | ] 71 | 72 | case :gen_udp.open(0, udp_options) do 73 | {:ok, socket} -> 74 | {:noreply, %State{state | mdns_socket: socket}} 75 | 76 | error -> 77 | {:stop, {:multicast, error}, state} 78 | end 79 | end 80 | 81 | def handle_info({:udp, socket, ip, _port, packet}, %{mdns_socket: socket} = state) do 82 | record = DNS.Record.decode(packet) 83 | state = handle_mdns(record.anlist, ip, state) 84 | {:noreply, state} 85 | end 86 | 87 | # i'm so sorry about this. It's not the correct way to do this. 88 | # I don't think an MDNS client is the solution to this long term 89 | # since it won't work on macos 90 | defp handle_mdns([%{type: :txt, domain: domain, data: data, ttl: ttl} | rest], ip, state) do 91 | state = 92 | case String.split(to_string(domain), ".") do 93 | [host, "_dartobservatory", "_tcp", "local"] -> 94 | case URI.decode_query(to_string(data)) do 95 | %{"port" => port, "path" => path} -> 96 | port = String.to_integer(port) 97 | uri = %URI{scheme: "http", host: "#{host}.local", port: port, path: path} 98 | 99 | duplicate_uri = 100 | Enum.find(state.discovered, fn 101 | {_, ^uri} -> true 102 | _ -> false 103 | end) 104 | 105 | if duplicate_uri do 106 | state 107 | else 108 | ref = make_ref() 109 | Process.send_after(self(), {:ttl, ref}, ttl * 1000) 110 | %{state | discovered: [{ref, uri} | state.discovered]} 111 | end 112 | 113 | _ -> 114 | state 115 | end 116 | 117 | _ -> 118 | state 119 | end 120 | 121 | handle_mdns(rest, ip, state) 122 | end 123 | 124 | defp handle_mdns([_unknown | rest], ip, state) do 125 | handle_mdns(rest, ip, state) 126 | end 127 | 128 | defp handle_mdns([], _ip, state) do 129 | state 130 | end 131 | 132 | defp send_query(socket, type, domain) do 133 | packet = %DNS.Record{ 134 | @query_packet 135 | | :qdlist => [ 136 | %DNS.Query{domain: domain, type: type, class: :in} 137 | ] 138 | } 139 | 140 | p = DNS.Record.encode(packet) 141 | :ok = :gen_udp.send(socket, @mdns_group, @mdns_port, p) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/flutter_embedder/platform_channel_message.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.PlatformChannelMessage do 2 | defstruct [:cookie, :channel, :message] 3 | 4 | @type cookie :: 0..255 5 | @type channel :: String.t() 6 | @type message :: binary() 7 | @type t() :: %__MODULE__{ 8 | cookie: cookie(), 9 | channel: channel(), 10 | message: message() 11 | } 12 | 13 | @type ok_response() :: {:ok, binary()} 14 | @type error_response() :: {:error, binary()} 15 | @type not_implemented_response() :: :not_implemented 16 | 17 | @spec decode(binary()) :: t() 18 | def decode( 19 | <> = data 21 | ) do 22 | %__MODULE__{ 23 | cookie: cookie, 24 | channel: channel, 25 | message: message 26 | } 27 | end 28 | 29 | @spec encode_response(t(), ok_response() | error_response() | not_implemented_response()) :: 30 | binary() 31 | def encode_response(%__MODULE__{cookie: cookie}, {:ok, value}) when is_binary(value) do 32 | <> 33 | end 34 | 35 | def encode_response(%__MODULE__{cookie: cookie}, {:error, value}) when is_binary(value) do 36 | <> 37 | end 38 | 39 | def encode_response(%__MODULE__{cookie: cookie}, :not_implemented) do 40 | <> 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/flutter_embedder/standard_message_codec.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.StandardMessageCodec do 2 | @kStdNull 0 3 | @kStdTrue 1 4 | @kStdFalse 2 5 | @kStdInt32 3 6 | @kStdInt64 4 7 | # @kStdLargeInt 5 # not used? 8 | @kStdFloat64 6 9 | @kStdString 7 10 | @kStdUInt8Array 8 11 | @kStdInt32Array 9 12 | @kStdInt64Array 10 13 | @kStdFloat64Array 11 14 | @kStdList 12 15 | @kStdMap 13 16 | 17 | @type dynamic_list() :: [value()] 18 | @type dart_map() :: map() 19 | @type int64() :: integer() 20 | @type int32() :: integer() 21 | @type float64() :: float() 22 | @type dart_string() :: binary() 23 | @type value :: t() 24 | @type t() :: dart_string() | float64() | int32() | int64() | dart_map() | dynamic_list() 25 | 26 | defmodule DecodeError do 27 | defexception [:message] 28 | end 29 | 30 | @doc "Checks if a value can be encoded into a Dart value" 31 | defguard is_valid_dart_value(value) 32 | when is_binary(value) or 33 | is_integer(value) or 34 | is_float(value) or 35 | is_boolean(value) or 36 | is_map(value) or 37 | is_list(value) 38 | 39 | @spec encode_value(value()) :: binary() 40 | def encode_value(nil), do: <<@kStdNull>> 41 | def encode_value(true), do: <<@kStdTrue>> 42 | def encode_value(false), do: <<@kStdFalse>> 43 | 44 | def encode_value(int32) when is_integer(int32) and abs(int32) <= 0x7FFFFFFF, 45 | do: <<@kStdInt32, int32::signed-native-32>> 46 | 47 | def encode_value(int64) when is_integer(int64) and abs(int64) <= 0x7FFFFFFFFFFFFFFF, 48 | do: <<@kStdInt64, int64::signed-native-64>> 49 | 50 | def encode_value(float64) when is_float(float64), 51 | do: <<@kStdFloat64, 0::6*8, float64::signed-native-float-64>> 52 | 53 | def encode_value(string) when is_binary(string) and byte_size(string) < 254 do 54 | <<@kStdString, byte_size(string)::8, string::binary>> 55 | end 56 | 57 | def encode_value(string) when is_binary(string) and byte_size(string) < 0xFFFF do 58 | <<@kStdString, 254, byte_size(string)::native-16, string::binary>> 59 | end 60 | 61 | # TODO encode @kStdUInt8Array, @kStdInt32Array, @kStdInt64Array, @kStdFloat64Array 62 | def encode_value(value) when is_list(value) do 63 | acc = <<@kStdList, length(value)::8>> 64 | 65 | Enum.reduce(value, acc, fn 66 | value, acc when is_valid_dart_value(value) -> 67 | acc <> encode_value(value) 68 | 69 | _invalid, _acc -> 70 | raise ArgumentError 71 | end) 72 | end 73 | 74 | # i don't think Dart actually allows for maps as return values via PlatformChannel 75 | def encode_value(%{} = map) do 76 | acc = <<@kStdMap, map_size(map)::8>> 77 | 78 | Enum.reduce(map, acc, fn 79 | # Dart only allows string keys 80 | {key, value}, acc when is_binary(key) and is_valid_dart_value(value) -> 81 | acc <> encode_value(key) <> encode_value(value) 82 | 83 | {_key, _value}, _acc -> 84 | raise ArgumentError 85 | end) 86 | end 87 | 88 | @spec decode_value(binary()) :: {value(), binary()} | no_return 89 | def decode_value(<<@kStdMap, num_pairs::8, map::binary>>) do 90 | decode_map(num_pairs, map, %{}) 91 | end 92 | 93 | def decode_value(<<@kStdList, num_items::8, values::binary>>) do 94 | decode_dynamic_list(num_items, values, []) 95 | end 96 | 97 | def decode_value(<<@kStdFloat64Array, num_items, float64_list::binary>>) do 98 | decode_float64_list(num_items, float64_list, []) 99 | end 100 | 101 | def decode_value(<<@kStdInt64Array, num_items, uint64_list::binary>>) do 102 | decode_uint64_list(num_items, uint64_list, []) 103 | end 104 | 105 | def decode_value(<<@kStdInt32Array, num_items, uint32_list::binary>>) do 106 | decode_uint32_list(num_items, uint32_list, []) 107 | end 108 | 109 | def decode_value(<<@kStdUInt8Array, num_items, uint8_list::binary>>) do 110 | decode_uint8_list(num_items, uint8_list, []) 111 | end 112 | 113 | def decode_value( 114 | <<@kStdString, 254, length::native-16, string::binary-size(length), rest::binary>> 115 | ), 116 | do: {string, rest} 117 | 118 | def decode_value(<<@kStdString, length::8, string::binary-size(length), rest::binary>>), 119 | do: {string, rest} 120 | 121 | def decode_value(<<@kStdFloat64, _pad::6*8, float64::signed-native-float-64, rest::binary>>), 122 | do: {float64, rest} 123 | 124 | def decode_value(<<@kStdInt64, int64::signed-native-64, rest::binary>>), do: {int64, rest} 125 | def decode_value(<<@kStdInt32, int32::signed-native-32, rest::binary>>), do: {int32, rest} 126 | def decode_value(<<@kStdFalse, rest::binary>>), do: {false, rest} 127 | def decode_value(<<@kStdTrue, rest::binary>>), do: {true, rest} 128 | def decode_value(<<@kStdNull, rest::binary>>), do: {nil, rest} 129 | 130 | def decode_value(<>) when type in 0..13 do 131 | raise DecodeError, message: "Could not decode known type: #{type}" 132 | end 133 | 134 | def decode_value(<>) do 135 | raise DecodeError, message: "Unknown type: #{inspect(type)}" 136 | end 137 | 138 | def decode_uint8_list(num_items, rest, acc) when length(acc) == num_items, 139 | do: {Enum.reverse(acc), rest} 140 | 141 | def decode_uint8_list(num_items, <>, acc) do 142 | decode_uint8_list(num_items, rest, [int8 | acc]) 143 | end 144 | 145 | def decode_uint32_list(num_items, rest, acc) when length(acc) == num_items, 146 | do: {Enum.reverse(acc), rest} 147 | 148 | def decode_uint32_list(num_items, <>, acc) do 149 | decode_uint32_list(num_items, rest, [int32 | acc]) 150 | end 151 | 152 | def decode_uint64_list(num_items, rest, acc) when length(acc) == num_items, 153 | do: {Enum.reverse(acc), rest} 154 | 155 | def decode_uint64_list(num_items, <>, acc) do 156 | decode_uint64_list(num_items, rest, [int64 | acc]) 157 | end 158 | 159 | def decode_float64_list(num_items, rest, acc) when length(acc) == num_items, 160 | do: {Enum.reverse(acc), rest} 161 | 162 | def decode_float64_list(num_items, <>, acc) do 163 | decode_float64_list(num_items, rest, [float | acc]) 164 | end 165 | 166 | def decode_map( 167 | num_pairs, 168 | <<@kStdString, _::binary>> = map, 169 | acc 170 | ) do 171 | {key, rest} = decode_value(map) 172 | {value, rest} = decode_value(rest) 173 | decode_map(num_pairs, rest, Map.put(acc, key, value)) 174 | end 175 | 176 | def decode_map(num_pairs, rest, map) when map_size(map) == num_pairs, do: {map, rest} 177 | 178 | def decode_dynamic_list(num_items, rest, acc) when length(acc) == num_items, 179 | do: {Enum.reverse(acc), rest} 180 | 181 | def decode_dynamic_list(num_items, items, acc) do 182 | {value, rest} = decode_value(items) 183 | decode_dynamic_list(num_items, rest, [value | acc]) 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /lib/flutter_embedder/standard_method_call.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.StandardMethodCall do 2 | alias FlutterEmbedder.{StandardMessageCodec, PlatformChannelMessage} 3 | defstruct [:method, :args] 4 | 5 | @type method :: StandardMessageCodec.dart_string() 6 | @type args :: StandardMessageCodec.t() 7 | @type t :: %__MODULE__{ 8 | method: method(), 9 | args: args() 10 | } 11 | 12 | @spec decode(PlatformChannelMessage.t()) :: {:ok, t()} | {:error, String.t()} 13 | def decode(%PlatformChannelMessage{message: message}) do 14 | with {method, args_} when is_binary(method) and byte_size(args_) > 0 <- 15 | StandardMessageCodec.decode_value(message), 16 | {args, ""} <- StandardMessageCodec.decode_value(args_) do 17 | {:ok, 18 | %__MODULE__{ 19 | method: method, 20 | args: args 21 | }} 22 | else 23 | {value, _rest} -> {:error, "Method must be a string. got: #{inspect(value)}"} 24 | end 25 | rescue 26 | e in [StandardMessageCodec.DecodeError] -> 27 | {:error, e.message} 28 | 29 | exception -> 30 | reraise(exception, __STACKTRACE__) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/flutter_embedder/standard_method_call/handler.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.MethodCall.Handler do 2 | @type std_ok :: {:ok, StandardMessageCodec.t()} 3 | @type std_error_code :: StandardMessageCodec.dart_string() 4 | @type std_error_message :: StandardMessageCodec.dart_string() 5 | @type std_err :: {:error, std_error_code, std_error_message, StandardMessageCodec.t()} 6 | @type std_not_implemented :: :not_implemented 7 | @type channel :: PlatformChannelMessage.channel() 8 | @type method :: StandardCall.method() 9 | @type args :: StandardCall.args() 10 | 11 | @doc """ 12 | https://flutter.dev/docs/development/platform-integration/platform-channels 13 | """ 14 | @callback handle_std_call(channel, method, args) :: std_ok | std_err | std_not_implemented 15 | end 16 | -------------------------------------------------------------------------------- /lib/flutter_embedder/stub_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.StubMethodCallHandler do 2 | require Logger 3 | 4 | def handle_std_call("samples.flutter.io/battery", "getBatteryLevel", _args) do 5 | {:ok, 69} 6 | end 7 | 8 | def handle_std_call(channel, method, args) do 9 | Logger.error("Unhandled std method call #{channel}:#{method}(#{inspect(args)}") 10 | :not_implemented 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/mix.tasks.flutter.discover.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Flutter.Discover do 2 | use Mix.Task 3 | alias FlutterEmbedder.MDNSClient 4 | 5 | def json(uri) do 6 | %{ 7 | name: "Nerves Flutter (#{uri.host})", 8 | request: "attach", 9 | deviceId: "flutter-tester", 10 | observatoryUri: to_string(%{uri | host: "localhost"}), 11 | type: "dart", 12 | program: "lib/main.dart" 13 | } 14 | |> Jason.encode!() 15 | end 16 | 17 | def run(_) do 18 | Mix.shell().info("Discovering devices via MDNS") 19 | 20 | with {:ok, pid} <- MDNSClient.start_link([]), 21 | {:ok, discovered} <- MDNSClient.discover(pid) do 22 | for uri <- discovered do 23 | cmd = "ssh -L #{uri.port}:localhost:#{uri.port} #{uri.host}" 24 | 25 | info = """ 26 | ============================================================= 27 | 28 | Found Flutter Observatory: #{uri.host} 29 | tunnel: #{cmd} 30 | url: #{to_string(%{uri | host: "localhost"})} 31 | launch.json: #{json(uri)} 32 | ============================================================= 33 | """ 34 | 35 | Mix.shell().info(info) 36 | end 37 | else 38 | {:error, reason} -> Mix.raise("Failed to discover via MDNS: #{inspect(reason)}") 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FlutterEmbedder.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :flutter_embedder, 7 | version: "0.1.0", 8 | elixir: "~> 1.10", 9 | start_permanent: Mix.env() == :prod, 10 | compilers: Mix.compilers() ++ [:elixir_make], 11 | make_targets: ["all"], 12 | make_clean: ["clean"], 13 | make_cwd: "src", 14 | deps: deps() 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:elixir_make, "~> 0.6.0", runtime: false}, 29 | {:jason, "~> 1.2"}, 30 | {:mdns_lite, "~> 0.6"} 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dns": {:hex, :dns, "2.2.0", "4721a79c2bccc25481930dffbfd06f40851321c3d679986af307111214bf124c", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm", "13ed1ef36ce896211ec6ce5e02709dbfb12aa61d6255bda8d531577a0a5a56e0"}, 3 | "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, 4 | "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, 5 | "mdns_lite": {:hex, :mdns_lite, "0.6.6", "f87475c3bb9c1bd55c1e912bbf2401820648d01bc2d0b0c48374e0067143af4a", [:mix], [{:dns, "~> 2.1", [hex: :dns, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7", [hex: :vintage_net, repo: "hexpm", optional: true]}], "hexpm", "c79e3259da3db9099213e0961efed5d72af3aa9c9563b27486da05dc99a0ac9a"}, 6 | "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"}, 7 | } 8 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = $(MIX_APP_PATH)/priv 2 | BUILD = $(MIX_APP_PATH)/obj 3 | 4 | SRC = erlcmd.c embedder.c embedder_platform_message.c 5 | 6 | ifeq ($(CROSSCOMPILE),) 7 | # Not Crosscompiled build 8 | SRC += embedder_glfw.c 9 | # glfw 10 | CFLAGS := -I/usr/include/GLFW/ -I/usr/include/GL/ 11 | CFLAGS += -g -DDEBUG 12 | LDFLAGS := -L$(PREFIX) -lGLEW -lX11 -lGLU -lGL -lglfw -lflutter_engine -lpthread -ldl 13 | else 14 | # Crosscompiled build 15 | SRC += embedder_drm.c 16 | # drm 17 | CFLAGS += $(shell pkg-config libdrm --cflags) 18 | CFLAGS += -DBUILD_ERLCMD_PLUGIN -Wall 19 | CFLAGS += -g -DDEBUG 20 | 21 | LDFLAGS += $(shell pkg-config libdrm --libs) 22 | # LDFLAGS += -ldrm -ldl -lgbm -lGLESv2 -lEGL -lglfw -pthread 23 | LDFLAGS += -ldrm -ldl -lgbm -lGLESv2 -lEGL -pthread 24 | LDFLAGS += -L$(PREFIX) -lflutter_engine 25 | endif 26 | 27 | all: $(PREFIX)/flutter_embedder 28 | 29 | $(PREFIX)/flutter_embedder: $(PREFIX) $(PREFIX)/libflutter_engine.so $(PREFIX)/icudtl.dat flutter_embedder.h ${SRC} 30 | $(CC) ${SRC} ${CFLAGS} $(LDFLAGS) -o $(PREFIX)/flutter_embedder 31 | 32 | ifeq ($(CROSSCOMPILE),) 33 | include precompiled_engine_x86_64.mk 34 | else 35 | # include precompiled_engine_armv7.mk 36 | include precompiled_engine_aarch64.mk 37 | endif 38 | 39 | format: 40 | astyle --style=kr --indent=spaces=4 --align-pointer=name \ 41 | --align-reference=name --convert-tabs --attach-namespaces \ 42 | --max-code-length=110 --max-instatement-indent=120 --pad-header \ 43 | --pad-oper \ 44 | embedder.c embedder_drm.c embedder_glfw.c embedder_platform_message.c 45 | 46 | clean: 47 | rm -rf $(PREFIX)/libflutter_engine.so flutter_embedder.h $(PREFIX)/flutter_embedder *.zip 48 | 49 | $(PREFIX) $(BUILD): 50 | mkdir -p $(PREFIX) 51 | 52 | .PHONY: all clean format 53 | -------------------------------------------------------------------------------- /src/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_EMBEDDER_DEBUG_H 2 | 3 | #ifdef DEBUG 4 | // #define LOG_PATH "/tmp/log.txt" 5 | #define log_location stderr 6 | #define debug(...) do { fprintf(log_location, __VA_ARGS__); fprintf(log_location, "\r\n"); fflush(log_location); } while(0) 7 | #define error(...) do { debug(__VA_ARGS__); } while (0) 8 | #else 9 | #define debug(...) 10 | #define error(...) 11 | #endif 12 | 13 | #endif -------------------------------------------------------------------------------- /src/embedder.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "embedder_platform_message.h" 15 | #include "embedder_gfx.h" 16 | #include "erlcmd.h" 17 | #include "flutter_embedder.h" 18 | #include "debug.h" 19 | 20 | static_assert(FLUTTER_ENGINE_VERSION == 1, 21 | "This Flutter Embedder was authored against the stable Flutter " 22 | "API at version 1. There has been a serious breakage in the " 23 | "API. Please read the ChangeLog and take appropriate action " 24 | "before updating this assertion"); 25 | 26 | FlutterEngine engine; 27 | 28 | static plat_msg_queue_t queue; 29 | static struct erlcmd handler; 30 | static struct pollfd fdset[3]; 31 | static int num_pollfds = 3; 32 | static int capstdout[2]; 33 | static unsigned char capstdoutbuffer[ERLCMD_BUF_SIZE]; 34 | static pthread_t flutter_embedder_pollfd_thread; 35 | 36 | /** Called by erlcmd. */ 37 | static void handle_from_elixir(const uint8_t *buffer, size_t length, void *cookie) 38 | { 39 | plat_msg_process(&queue, engine, buffer, length); 40 | } 41 | 42 | /** gfx implementations must configure this function */ 43 | void on_platform_message( 44 | const FlutterPlatformMessage *message, 45 | void *userdata 46 | ) 47 | { 48 | debug("on_platform_message"); 49 | plat_msg_push(&queue, message); 50 | eventfd_write(fdset[2].fd, 1); 51 | } 52 | 53 | /** Initializes erlcmd to be be ready for polling */ 54 | static int init_erlcmd() 55 | { 56 | int writefd = dup(STDOUT_FILENO); 57 | int readfd = dup(STDIN_FILENO); 58 | erlcmd_init(&handler, readfd, writefd, handle_from_elixir, NULL); 59 | // Initialize the file descriptor set for polling 60 | memset(fdset, -1, sizeof(fdset)); 61 | fdset[0].fd = readfd; 62 | fdset[0].events = POLLIN; 63 | fdset[0].revents = 0; 64 | return 0; 65 | } 66 | 67 | /** Initializes the engine stdout and eventfd pollfds */ 68 | static int init_pollfds() 69 | { 70 | if (pipe2(capstdout, O_NONBLOCK) < 0) { 71 | error("pipe2"); 72 | return -1; 73 | } 74 | 75 | // replace STDOUT with something we can poll 76 | // this is because the Flutter engine doesn't allow 77 | // for configuration of it's output. 78 | dup2(capstdout[1], STDOUT_FILENO); 79 | fdset[1].fd = capstdout[0]; 80 | fdset[1].events = POLLIN; 81 | fdset[1].revents = 0; 82 | 83 | // EventFD for signaling that 84 | // Tasks have been resolved 85 | fdset[2].fd = eventfd(0, 0); 86 | fdset[2].events = POLLIN; 87 | fdset[2].revents = 0; 88 | return 0; 89 | } 90 | 91 | // Eventloop for for I/O with the port and the engine 92 | void *pollfd_thread_function(void *vargp) 93 | { 94 | for (;;) { 95 | for (int i = 0; i < num_pollfds; i++) 96 | fdset[i].revents = 0; 97 | int rc = poll(fdset, num_pollfds, 0); 98 | if (rc < 0) { 99 | // Retry if EINTR 100 | if (errno == EINTR) 101 | continue; 102 | error("poll failed with %d", errno); 103 | } 104 | 105 | // Erlang closed the port 106 | if (fdset[0].revents & POLLHUP) 107 | exit(2); 108 | 109 | // from elixir 110 | if (fdset[0].revents & POLLIN) 111 | erlcmd_process(&handler); 112 | 113 | // Engine STDOUT 114 | if (fdset[1].revents & POLLIN) { 115 | memset(capstdoutbuffer, 0, ERLCMD_BUF_SIZE); 116 | capstdoutbuffer[sizeof(uint32_t)] = 1; 117 | size_t nbytes = read(fdset[1].fd, capstdoutbuffer + sizeof(uint32_t) + sizeof(uint32_t), 118 | ERLCMD_BUF_SIZE - sizeof(uint32_t) - sizeof(uint32_t)); 119 | if (nbytes < 0) 120 | error("Failed to read engine log buffer"); 121 | erlcmd_send(&handler, capstdoutbuffer, nbytes); 122 | } 123 | 124 | if (fdset[2].revents & (POLLIN | POLLHUP)) { 125 | eventfd_t event; 126 | eventfd_read(fdset[1].fd, &event); 127 | size_t r; 128 | r = plat_msg_dispatch_all(&queue, &handler); 129 | if (r < 0) 130 | error("Failed to dispatch platform messages: %ld", r); 131 | } 132 | } 133 | } 134 | 135 | int main(int argc, const char *argv[]) 136 | { 137 | // #ifdef DEBUG 138 | // #ifdef LOG_PATH 139 | // log_location = fopen(LOG_PATH, "w"); 140 | // #endif 141 | // #endif 142 | if (argc != 3) { 143 | error("Invalid Arguments"); 144 | exit(EXIT_FAILURE); 145 | } 146 | 147 | const char *project_path = argv[1]; 148 | const char *icudtl_path = argv[2]; 149 | 150 | if (init_erlcmd() < 0) { 151 | error("erlcmd"); 152 | exit(EXIT_FAILURE); 153 | } 154 | 155 | if (plat_msg_queue_init(&queue) < 0) { 156 | error("plat_msg_init"); 157 | exit(EXIT_FAILURE); 158 | } 159 | 160 | if (init_pollfds() < 0) { 161 | error("poll"); 162 | exit(EXIT_FAILURE); 163 | } 164 | 165 | FlutterProjectArgs args = { 166 | .struct_size = sizeof(FlutterProjectArgs), 167 | .assets_path = project_path, 168 | .icu_data_path = icudtl_path, 169 | .platform_message_callback = on_platform_message, 170 | .vsync_callback = gfx_vsync 171 | }; 172 | FlutterRendererConfig config = { 173 | .type = kOpenGL, 174 | .open_gl.struct_size = sizeof(FlutterOpenGLRendererConfig), 175 | .open_gl.make_current = gfx_make_current, 176 | .open_gl.clear_current = gfx_clear_current, 177 | .open_gl.present = gfx_present, 178 | .open_gl.fbo_callback = gfx_fbo_callback, 179 | .open_gl.gl_proc_resolver = proc_resolver 180 | }; 181 | 182 | FlutterTaskRunnerDescription custom_task_runner_description = { 183 | .struct_size = sizeof(FlutterTaskRunnerDescription), 184 | .user_data = NULL, 185 | .runs_task_on_current_thread_callback = runs_platform_tasks_on_current_thread, 186 | .post_task_callback = on_post_flutter_task 187 | }; 188 | 189 | FlutterCustomTaskRunners custom_task_runners = { 190 | .struct_size = sizeof(FlutterCustomTaskRunners), 191 | .platform_task_runner = &custom_task_runner_description 192 | }; 193 | 194 | args.custom_task_runners = &custom_task_runners; 195 | 196 | debug("FlutterEngineInitialize start"); 197 | FlutterEngineResult result = FlutterEngineInitialize(FLUTTER_ENGINE_VERSION, &config, &args, NULL, &engine); 198 | assert(result == kSuccess && engine != NULL); 199 | debug("FlutterEngineInitialize end"); 200 | 201 | debug("initializing gfx"); 202 | if (gfx_init(engine) < 0) { 203 | error("gfx"); 204 | exit(EXIT_FAILURE); 205 | } 206 | 207 | debug("FlutterEngineRunInitialized start"); 208 | result = FlutterEngineRunInitialized(engine); 209 | debug("FlutterEngineRunInitialized end"); 210 | assert(result == kSuccess && engine != NULL); 211 | 212 | // FlutterEngineResult result = FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, NULL, engine); 213 | debug("and here"); 214 | 215 | if (pthread_create(&flutter_embedder_pollfd_thread, 216 | NULL, 217 | pollfd_thread_function, 218 | NULL) < 0) { 219 | error("pthread"); 220 | exit(EXIT_FAILURE); 221 | } 222 | 223 | // Enter the main loop 224 | gfx_loop(); 225 | error("gfx_loop exit"); 226 | 227 | if (pthread_join(flutter_embedder_pollfd_thread, NULL) < 0) { 228 | error("pthread_join"); 229 | exit(EXIT_FAILURE); 230 | } 231 | 232 | if (plat_msg_queue_destroy(&queue) < 0) { 233 | error("plat_msg_queue_destroy"); 234 | exit(EXIT_FAILURE); 235 | } 236 | 237 | if (gfx_terminate() < 0) { 238 | error("gfx_terminate"); 239 | exit(EXIT_FAILURE); 240 | } 241 | exit(EXIT_SUCCESS); 242 | } -------------------------------------------------------------------------------- /src/embedder_drm.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | #include "flutter_embedder.h" 45 | #include "debug.h" 46 | 47 | #define EGL_PLATFORM_GBM_KHR 0x31D7 48 | 49 | typedef enum { 50 | kVBlankRequest, 51 | kVBlankReply, 52 | kFlutterTask 53 | } engine_task_type; 54 | 55 | struct engine_task { 56 | struct engine_task *next; 57 | engine_task_type type; 58 | union { 59 | FlutterTask task; 60 | struct { 61 | uint64_t vblank_ns; 62 | intptr_t baton; 63 | }; 64 | }; 65 | uint64_t target_time; 66 | }; 67 | 68 | struct drm_fb { 69 | struct gbm_bo *bo; 70 | uint32_t fb_id; 71 | }; 72 | 73 | struct pageflip_data { 74 | struct gbm_bo *releaseable_bo; 75 | intptr_t next_baton; 76 | }; 77 | 78 | 79 | /// width & height of the display in pixels 80 | // static uint32_t width, height; 81 | 82 | // /// physical width & height of the display in millimeters 83 | // /// the physical size can only be queried for HDMI displays (and even then, most displays will 84 | // /// probably return bogus values like 160mm x 90mm). 85 | // /// for DSI displays, the physical size of the official 7-inch display will be set in init_display. 86 | // /// init_display will only update width_mm and height_mm if they are set to zero, allowing you 87 | // /// to hardcode values for you individual display. 88 | // static uint32_t width_mm = 0, height_mm = 0; 89 | // static uint32_t refresh_rate = 60; 90 | static uint32_t refresh_period_ns = 16666666; 91 | 92 | /// The pixel ratio used by flutter. 93 | /// This is computed inside init_display using width_mm and height_mm. 94 | /// flutter only accepts pixel ratios >= 1.0 95 | /// init_display will only update this value if it is equal to zero, 96 | /// allowing you to hardcode values. 97 | // static double pixel_ratio = 1.358234; 98 | 99 | static struct { 100 | char device[PATH_MAX]; 101 | bool has_device; 102 | int fd; 103 | uint32_t connector_id; 104 | drmModeModeInfo *mode; 105 | uint32_t crtc_id; 106 | size_t crtc_index; 107 | struct gbm_bo *previous_bo; 108 | drmEventContext evctx; 109 | } drm = {0}; 110 | 111 | static struct { 112 | struct gbm_device *device; 113 | struct gbm_surface *surface; 114 | uint32_t format; 115 | uint64_t modifier; 116 | } gbm = {0}; 117 | 118 | static struct { 119 | EGLDisplay display; 120 | EGLConfig config; 121 | EGLContext context; 122 | EGLSurface surface; 123 | 124 | bool modifiers_supported; 125 | char *renderer; 126 | 127 | EGLDisplay (*eglGetPlatformDisplayEXT)(EGLenum platform, void *native_display, const EGLint *attrib_list); 128 | EGLSurface (*eglCreatePlatformWindowSurfaceEXT)(EGLDisplay dpy, EGLConfig config, void *native_window, 129 | const EGLint *attrib_list); 130 | EGLSurface (*eglCreatePlatformPixmapSurfaceEXT)(EGLDisplay dpy, EGLConfig config, void *native_pixmap, 131 | const EGLint *attrib_list); 132 | } egl = {0}; 133 | 134 | // position & pointer phase of a mouse pointer / multitouch slot 135 | // A 10-finger multi-touch display has 10 slots and each of them have their own position, tracking id, etc. 136 | // All mouses / touchpads share the same mouse pointer. 137 | struct mousepointer_mtslot { 138 | // the MT tracking ID used to track this touch. 139 | int id; 140 | int32_t flutter_slot_id; 141 | double x, y; 142 | FlutterPointerPhase phase; 143 | }; 144 | 145 | static struct mousepointer_mtslot mousepointer; 146 | static pthread_t io_thread_id; 147 | #define MAX_EVENTS_PER_READ 64 148 | static struct input_event io_input_buffer[MAX_EVENTS_PER_READ]; 149 | 150 | // Flutter VSync handles 151 | // stored as a ring buffer. i_batons is the offset of the first baton (0 - 63) 152 | // scheduled_frames - 1 is the number of total number of stored batons. 153 | // (If 5 vsync events were asked for by the flutter engine, you only need to store 4 batons. 154 | // The baton for the first one that was asked for would've been returned immediately.) 155 | static intptr_t batons[64]; 156 | static uint8_t i_batons = 0; 157 | static int scheduled_frames = 0; 158 | static pthread_t platform_thread_id; 159 | 160 | static struct engine_task *tasklist = NULL; 161 | static pthread_mutex_t tasklist_lock = PTHREAD_MUTEX_INITIALIZER; 162 | static pthread_cond_t task_added = PTHREAD_COND_INITIALIZER; 163 | 164 | extern FlutterEngine engine; 165 | static _Atomic bool engine_running = false; 166 | 167 | static void _post_platform_task(struct engine_task *); 168 | 169 | static void pageflip_handler(int fd, unsigned int frame, unsigned int sec, unsigned int usec, void *userdata) 170 | { 171 | debug("pageflip start"); 172 | FlutterEngineTraceEventInstant("pageflip"); 173 | _post_platform_task(&(struct engine_task) { 174 | .type = kVBlankReply, 175 | .target_time = 0, 176 | .vblank_ns = sec * 1000000000ull + usec * 1000ull, 177 | }); 178 | debug("pageflip end"); 179 | } 180 | 181 | static void drm_fb_destroy_callback(struct gbm_bo *bo, void *data) 182 | { 183 | struct drm_fb *fb = data; 184 | 185 | if (fb->fb_id) 186 | drmModeRmFB(drm.fd, fb->fb_id); 187 | 188 | free(fb); 189 | } 190 | 191 | static struct drm_fb *drm_fb_get_from_bo(struct gbm_bo *bo) 192 | { 193 | // if the buffer object already has some userdata associated with it, 194 | // it's the framebuffer we allocated. 195 | struct drm_fb *fb = gbm_bo_get_user_data(bo); 196 | if (fb) return fb; 197 | 198 | // if there's no framebuffer for the bo, we need to create one. 199 | fb = calloc(1, sizeof(struct drm_fb)); 200 | fb->bo = bo; 201 | 202 | int width = gbm_bo_get_width(bo); 203 | int height = gbm_bo_get_height(bo); 204 | int format = gbm_bo_get_format(bo); 205 | 206 | uint64_t modifiers[4] = {0}; 207 | modifiers[0] = gbm_bo_get_modifier(bo); 208 | const int num_planes = gbm_bo_get_plane_count(bo); 209 | 210 | uint32_t strides[4] = {0}, handles[4] = {0}, offsets[4] = {0}; 211 | for (int i = 0; i < num_planes; i++) { 212 | strides[i] = gbm_bo_get_stride_for_plane(bo, i); 213 | handles[i] = gbm_bo_get_handle(bo).u32; 214 | offsets[i] = gbm_bo_get_offset(bo, i); 215 | modifiers[i] = modifiers[0]; 216 | } 217 | 218 | uint32_t flags = 0; 219 | if (modifiers[0]) { 220 | flags = DRM_MODE_FB_MODIFIERS; 221 | } 222 | 223 | int ok = drmModeAddFB2WithModifiers(drm.fd, width, height, format, handles, strides, offsets, modifiers, 224 | &fb->fb_id, flags); 225 | 226 | if (ok) { 227 | if (flags) 228 | debug("drm_fb_get_from_bo: modifiers failed!"); 229 | 230 | memcpy(handles, (uint32_t [4]) { 231 | gbm_bo_get_handle(bo).u32, 0, 0, 0 232 | }, 16); 233 | 234 | memcpy(strides, (uint32_t [4]) { 235 | gbm_bo_get_stride(bo), 0, 0, 0 236 | }, 16); 237 | 238 | memset(offsets, 0, 16); 239 | 240 | ok = drmModeAddFB2(drm.fd, width, height, format, handles, strides, offsets, &fb->fb_id, 0); 241 | } 242 | 243 | if (ok) { 244 | debug("drm_fb_get_from_bo: failed to create fb: %s\n", strerror(errno)); 245 | free(fb); 246 | return NULL; 247 | } 248 | 249 | gbm_bo_set_user_data(bo, fb, drm_fb_destroy_callback); 250 | 251 | return fb; 252 | } 253 | 254 | static bool run_message_loop(void) 255 | { 256 | debug("run_message_loop pre"); 257 | while (true) { 258 | debug("run_message_loop start"); 259 | pthread_mutex_lock(&tasklist_lock); 260 | 261 | // wait for a task to be inserted into the list 262 | while (tasklist == NULL) 263 | pthread_cond_wait(&task_added, &tasklist_lock); 264 | 265 | // wait for a task to be ready to be run 266 | uint64_t currenttime; 267 | while (tasklist->target_time > (currenttime = FlutterEngineGetCurrentTime())) { 268 | struct timespec abstargetspec; 269 | clock_gettime(CLOCK_REALTIME, &abstargetspec); 270 | uint64_t abstarget = abstargetspec.tv_nsec + abstargetspec.tv_sec * 1000000000ull + 271 | (tasklist->target_time - currenttime); 272 | abstargetspec.tv_nsec = abstarget % 1000000000; 273 | abstargetspec.tv_sec = abstarget / 1000000000; 274 | 275 | pthread_cond_timedwait(&task_added, &tasklist_lock, &abstargetspec); 276 | } 277 | 278 | struct engine_task *task = tasklist; 279 | tasklist = tasklist->next; 280 | 281 | pthread_mutex_unlock(&tasklist_lock); 282 | if (task->type == kVBlankRequest) { 283 | if (scheduled_frames == 0) { 284 | uint64_t ns; 285 | drmCrtcGetSequence(drm.fd, drm.crtc_id, NULL, &ns); 286 | FlutterEngineOnVsync(engine, task->baton, ns, ns + refresh_period_ns); 287 | } else { 288 | batons[(i_batons + (scheduled_frames - 1)) & 63] = task->baton; 289 | 290 | } 291 | scheduled_frames++; 292 | } else if (task->type == kVBlankReply) { 293 | if (scheduled_frames > 1) { 294 | intptr_t baton = batons[i_batons]; 295 | i_batons = (i_batons + 1) & 63; 296 | uint64_t ns = task->vblank_ns; 297 | FlutterEngineOnVsync(engine, baton, ns, ns + refresh_period_ns); 298 | } 299 | scheduled_frames--; 300 | } else if (task->type == kFlutterTask) { 301 | if (FlutterEngineRunTask(engine, &task->task) != kSuccess) { 302 | debug("Error running platform task"); 303 | return false; 304 | } 305 | } 306 | 307 | free(task); 308 | debug("run_message_loop end"); 309 | } 310 | debug("run_message_loop exit"); 311 | return true; 312 | } 313 | 314 | static drmModeConnector *find_connector(drmModeRes *resources) 315 | { 316 | debug("find_connector"); 317 | debug("find_connector %d", resources->count_connectors); 318 | // iterate the connectors 319 | for (int i = 0; i < resources->count_connectors; i++) { 320 | debug("checking connector"); 321 | drmModeConnector *connector = drmModeGetConnector(drm.fd, resources->connectors[i]); 322 | if (connector->connection == DRM_MODE_CONNECTED) 323 | return connector; 324 | debug("not that one"); 325 | drmModeFreeConnector(connector); 326 | } 327 | // no connector found 328 | return NULL; 329 | } 330 | 331 | static drmModeEncoder *find_encoder(drmModeRes *resources, drmModeConnector *connector) 332 | { 333 | if (connector->encoder_id) 334 | return drmModeGetEncoder(drm.fd, connector->encoder_id); 335 | // no encoder found 336 | return NULL; 337 | } 338 | 339 | static size_t init_drm(void) 340 | { 341 | drm.fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); 342 | debug("opened card\r\n"); 343 | 344 | debug("drmModeGetResources before"); 345 | drmModeRes *resources = drmModeGetResources(drm.fd); 346 | if(!resources) { 347 | error("drmModeGetResources failed"); 348 | return false; 349 | } 350 | debug("drmModeGetResources after"); 351 | // find a connector 352 | debug("find_connector before"); 353 | drmModeConnector *connector = find_connector(resources); 354 | debug("find_connector after"); 355 | if (!connector) { 356 | debug("Failed to get connector"); 357 | return -1; 358 | } 359 | debug("found connector"); 360 | 361 | // save the connector_id 362 | drm.connector_id = connector->connector_id; 363 | 364 | // save the first mode 365 | drm.mode = &connector->modes[0]; 366 | debug("resolution: %ix%i\n", drm.mode->hdisplay, drm.mode->vdisplay); 367 | 368 | // find an encoder 369 | drmModeEncoder *encoder = find_encoder(resources, connector); 370 | if (!encoder) { 371 | debug("failed to get encoder\r\n"); 372 | return -1; 373 | } 374 | // find a CRTC 375 | if (encoder->crtc_id) { 376 | drm.crtc_id = encoder->crtc_id; 377 | // crtc = drmModeGetCrtc(drm.fd, encoder->crtc_id); 378 | } 379 | drmModeFreeEncoder(encoder); 380 | 381 | // fix this 382 | //drmModeFreeConnector(connector); 383 | drmModeFreeResources(resources); 384 | return 0; 385 | } 386 | 387 | static size_t init_opengl(void) 388 | { 389 | debug("setup opengl"); 390 | gbm.device = gbm_create_device(drm.fd); 391 | egl.display = eglGetDisplay(gbm.device); 392 | 393 | if (!eglInitialize(egl.display, NULL, NULL)) { 394 | debug("failed to initialize egl"); 395 | return -1; 396 | } 397 | 398 | // create an OpenGL context 399 | if(!eglBindAPI(EGL_OPENGL_API)){ 400 | debug("failed to bind api"); 401 | return -1; 402 | } 403 | 404 | const EGLint attributes[] = { 405 | EGL_RED_SIZE,8, 406 | EGL_GREEN_SIZE,8, 407 | EGL_BLUE_SIZE,8, 408 | EGL_ALPHA_SIZE,8, 409 | EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, 410 | EGL_SAMPLES, 0, 411 | EGL_NONE, 412 | }; 413 | 414 | const char *egl_exts_client; 415 | 416 | debug("Querying EGL client extensions..."); 417 | egl_exts_client = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); 418 | debug("%s", egl_exts_client); 419 | 420 | egl.eglGetPlatformDisplayEXT = (void *) eglGetProcAddress("eglGetPlatformDisplayEXT"); 421 | debug("Getting EGL display for GBM device..."); 422 | if (egl.eglGetPlatformDisplayEXT) egl.display = egl.eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gbm.device, NULL); 423 | else 424 | egl.display = eglGetDisplay((void *) gbm.device); 425 | 426 | if (!egl.display) { 427 | debug("Couldn't get EGL display"); 428 | return -1; 429 | } 430 | 431 | EGLint num_config; 432 | if(!eglChooseConfig(egl.display, attributes, &egl.config, 1, &num_config)) { 433 | debug("failed to choose config"); 434 | return -1; 435 | } 436 | 437 | egl.context = eglCreateContext(egl.display, egl.config, EGL_NO_CONTEXT, NULL); 438 | if (!egl.context) { 439 | debug("Failed to create context"); 440 | return -1; 441 | } 442 | 443 | gbm.format = DRM_FORMAT_XRGB8888; 444 | 445 | // create the GBM and EGL surface 446 | gbm.surface = gbm_surface_create(gbm.device, drm.mode->hdisplay, drm.mode->vdisplay, gbm.format, 1); 447 | if (!gbm.surface) { 448 | debug("Failed to create surface\r\n"); 449 | return -1; 450 | } 451 | 452 | egl.surface = eglCreateWindowSurface(egl.display, egl.config, gbm.surface, NULL); 453 | if (!egl.surface) { 454 | debug("failed to create window surface"); 455 | return -1; 456 | } 457 | 458 | if (!eglMakeCurrent(egl.display, egl.surface, egl.surface, egl.context)) { 459 | debug("Could not make EGL context current to get OpenGL information"); 460 | return -1; 461 | } 462 | debug("setup opengl complete"); 463 | return 0; 464 | } 465 | 466 | static size_t init_display() 467 | { 468 | debug("\r\ninit_display start"); 469 | size_t result; 470 | result = init_drm(); 471 | if(result != 0) 472 | return result; 473 | debug("init_drm result: %ld", result); 474 | 475 | result = init_opengl(); 476 | if(result != 0) 477 | return result; 478 | 479 | debug("init_opengl result: %ld", result); 480 | 481 | drm.evctx = (drmEventContext) { 482 | .version = 4, 483 | .vblank_handler = NULL, 484 | .page_flip_handler = pageflip_handler, 485 | .page_flip_handler2 = NULL, 486 | .sequence_handler = NULL 487 | }; 488 | 489 | debug("Swapping buffers..."); 490 | eglSwapBuffers(egl.display, egl.surface); 491 | 492 | debug("Locking front buffer..."); 493 | drm.previous_bo = gbm_surface_lock_front_buffer(gbm.surface); 494 | 495 | debug("getting new framebuffer for BO..."); 496 | struct drm_fb *fb = drm_fb_get_from_bo(drm.previous_bo); 497 | if (!fb) { 498 | debug("failed to get a new framebuffer BO"); 499 | return -1; 500 | } 501 | 502 | debug("Setting CRTC..."); 503 | result = drmModeSetCrtc(drm.fd, drm.crtc_id, fb->fb_id, 0, 0, &drm.connector_id, 1, drm.mode); 504 | if (result) { 505 | debug("failed to set mode: %s\n", strerror(errno)); 506 | return -1; 507 | } 508 | 509 | debug("Clearing current context..."); 510 | if (!eglMakeCurrent(egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { 511 | debug("Could not clear EGL context"); 512 | return -1; 513 | } 514 | 515 | debug("init_display end\r\n"); 516 | return 0; 517 | } 518 | 519 | static size_t init_io(void) 520 | { 521 | FlutterPointerEvent flutterevents[16] = {0}; 522 | size_t i_flutterevent = 0; 523 | int n_flutter_slots = 0; 524 | 525 | // add the mouse slot 526 | mousepointer = (struct mousepointer_mtslot) { 527 | .id = 0, 528 | .flutter_slot_id = n_flutter_slots++, 529 | .x = 0, .y = 0, 530 | .phase = kCancel 531 | }; 532 | 533 | flutterevents[i_flutterevent++] = (FlutterPointerEvent) { 534 | .struct_size = sizeof(FlutterPointerEvent), 535 | .phase = kAdd, 536 | .timestamp = (size_t) (FlutterEngineGetCurrentTime() * 1000), 537 | .x = 0, 538 | .y = 0, 539 | .signal_kind = kFlutterPointerSignalKindNone, 540 | .device_kind = kFlutterPointerDeviceKindTouch, 541 | .device = mousepointer.flutter_slot_id, 542 | .buttons = 0 543 | }; 544 | if(FlutterEngineSendPointerEvent(engine, flutterevents, i_flutterevent) != kSuccess) { 545 | return -1; 546 | } 547 | return 0; 548 | } 549 | 550 | static void process_io_events(int fd) 551 | { 552 | // Read as many the input events as possible 553 | ssize_t rd = read(fd, io_input_buffer, sizeof(io_input_buffer)); 554 | if (rd < 0) 555 | error("read failed"); 556 | if (rd % sizeof(struct input_event)) 557 | error("read returned %d which is not a multiple of %d!", (int) rd, (int) sizeof(struct input_event)); 558 | 559 | FlutterPointerEvent flutterevents[64] = {0}; 560 | size_t i_flutterevent = 0; 561 | 562 | size_t event_count = rd / sizeof(struct input_event); 563 | for (size_t i = 0; i < event_count; i++) { 564 | if (io_input_buffer[i].type == EV_ABS) { 565 | if (io_input_buffer[i].code == ABS_X) { 566 | mousepointer.x = io_input_buffer[i].value; 567 | } else if (io_input_buffer[i].code == ABS_Y) { 568 | mousepointer.y = io_input_buffer[i].value; 569 | } else if (io_input_buffer[i].code == ABS_MT_TRACKING_ID && io_input_buffer[i].value == -1) { 570 | } 571 | 572 | if (mousepointer.phase == kDown) { 573 | flutterevents[i_flutterevent++] = (FlutterPointerEvent) { 574 | .struct_size = sizeof(FlutterPointerEvent), 575 | .phase = kMove, 576 | .timestamp = io_input_buffer[i].time.tv_sec * 1000000 + io_input_buffer[i].time.tv_usec, 577 | .x = mousepointer.x, .y = mousepointer.y, 578 | .device = mousepointer.flutter_slot_id, 579 | .signal_kind = kFlutterPointerSignalKindNone, 580 | .device_kind = kFlutterPointerDeviceKindTouch, 581 | .buttons = 0 582 | }; 583 | } 584 | 585 | } else if (io_input_buffer[i].type == EV_KEY) { 586 | if (io_input_buffer[i].code == BTN_TOUCH) { 587 | mousepointer.phase = io_input_buffer[i].value ? kDown : kUp; 588 | } else { 589 | debug("unknown EV_KEY code=%d value=%d\r\n", io_input_buffer[i].code, io_input_buffer[i].value); 590 | } 591 | } else if (io_input_buffer[i].type == EV_SYN && io_input_buffer[i].code == SYN_REPORT) { 592 | // we don't want to send an event to flutter if nothing changed. 593 | if (mousepointer.phase == kCancel) continue; 594 | 595 | flutterevents[i_flutterevent++] = (FlutterPointerEvent) { 596 | .struct_size = sizeof(FlutterPointerEvent), 597 | .phase = mousepointer.phase, 598 | .timestamp = io_input_buffer[i].time.tv_sec * 1000000 + io_input_buffer[i].time.tv_usec, 599 | .x = mousepointer.x, .y = mousepointer.y, 600 | .device = mousepointer.flutter_slot_id, 601 | .signal_kind = kFlutterPointerSignalKindNone, 602 | .device_kind = kFlutterPointerDeviceKindTouch, 603 | .buttons = 0 604 | }; 605 | if (mousepointer.phase == kUp) 606 | mousepointer.phase = kCancel; 607 | } else { 608 | debug("unknown input_event type=%d\r\n", io_input_buffer[i].type); 609 | } 610 | } 611 | 612 | if (i_flutterevent == 0) return; 613 | 614 | // now, send the data to the flutter engine 615 | if (FlutterEngineSendPointerEvent(engine, flutterevents, i_flutterevent) != kSuccess) { 616 | debug("could not send pointer events to flutter engine\r\n"); 617 | } 618 | } 619 | 620 | static void *io_loop(void *userdata) 621 | { 622 | const char *input_path = "/dev/input/event0"; 623 | int fd = open(input_path, O_RDONLY); 624 | if (errno == EACCES && getuid() != 0) 625 | error("You do not have access to %s.", input_path); 626 | 627 | struct pollfd fdset[2]; 628 | memset(fdset, -1, sizeof(fdset)); 629 | 630 | fdset[0].fd = fd; 631 | fdset[0].events = (POLLIN | POLLPRI | POLLHUP); 632 | fdset[0].revents = 0; 633 | 634 | fdset[1].fd = drm.fd; 635 | fdset[1].events = (POLLIN | POLLPRI | POLLHUP); 636 | fdset[1].revents = 0; 637 | while (engine_running) { 638 | 639 | int rc = poll(fdset, 2, -1); 640 | if (rc < 0) 641 | error("poll error"); 642 | 643 | if (fdset[0].revents & (POLLIN | POLLHUP)) 644 | process_io_events(fd); 645 | 646 | if (fdset[1].revents & (POLLIN | POLLHUP)) 647 | drmHandleEvent(drm.fd, &drm.evctx); 648 | } 649 | return NULL; 650 | } 651 | 652 | static size_t run_io_thread() 653 | { 654 | debug("run_io_thread start"); 655 | int ok = pthread_create(&io_thread_id, NULL, &io_loop, NULL); 656 | if (ok != 0) { 657 | error("couldn't create io thread: [%s]", strerror(ok)); 658 | return false; 659 | } 660 | 661 | ok = pthread_setname_np(io_thread_id, "io"); 662 | if (ok != 0) { 663 | error("couldn't set name of io thread: [%s]", strerror(ok)); 664 | return false; 665 | } 666 | 667 | debug("run_io_thread end"); 668 | return 0; 669 | } 670 | 671 | bool gfx_make_current(void *userdata) 672 | { 673 | debug("gfx_make_current start"); 674 | if (eglMakeCurrent(egl.display, egl.surface, egl.surface, egl.context) != EGL_TRUE) { 675 | debug("make_current: could not make the context current."); 676 | return false; 677 | } 678 | debug("gfx_make_current end"); 679 | return true; 680 | } 681 | 682 | bool gfx_clear_current(void *userdata) 683 | { 684 | debug("gfx_clear_current start"); 685 | if (eglMakeCurrent(egl.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) != EGL_TRUE) { 686 | debug("clear_current: could not clear the current context."); 687 | return false; 688 | } 689 | debug("gfx_clear_current end"); 690 | return true; 691 | } 692 | 693 | bool gfx_present(void *userdata) 694 | { 695 | debug("gfx_present start"); 696 | FlutterEngineTraceEventDurationBegin("present"); 697 | 698 | eglSwapBuffers(egl.display, egl.surface); 699 | struct gbm_bo *next_bo = gbm_surface_lock_front_buffer(gbm.surface); 700 | struct drm_fb *fb = drm_fb_get_from_bo(next_bo); 701 | 702 | int ok = drmModePageFlip(drm.fd, drm.crtc_id, fb->fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL); 703 | if (ok) { 704 | perror("failed to queue page flip"); 705 | return false; 706 | } 707 | 708 | gbm_surface_release_buffer(gbm.surface, drm.previous_bo); 709 | drm.previous_bo = next_bo; 710 | 711 | FlutterEngineTraceEventDurationEnd("present"); 712 | debug("gfx_present end"); 713 | return true; 714 | } 715 | 716 | uint32_t gfx_fbo_callback(void *userdata) 717 | { 718 | return 0; 719 | } 720 | 721 | void gfx_vsync(void *userdata, intptr_t baton) 722 | { 723 | debug("vsync start"); 724 | _post_platform_task(&(struct engine_task) { 725 | .type = kVBlankRequest, 726 | .target_time = 0, 727 | .baton = baton 728 | }); 729 | debug("vsync end"); 730 | } 731 | 732 | void on_post_flutter_task(FlutterTask task, uint64_t target_time, void *userdata) 733 | { 734 | debug("on_post_flutter_task start"); 735 | _post_platform_task(&(struct engine_task) { 736 | .type = kFlutterTask, 737 | .task = task, 738 | .target_time = target_time 739 | }); 740 | debug("on_post_flutter_task end"); 741 | } 742 | 743 | bool runs_platform_tasks_on_current_thread(void *userdata) 744 | { 745 | debug("runs_platform_tasks_on_current_thread"); 746 | return pthread_equal(pthread_self(), platform_thread_id) != 0; 747 | } 748 | 749 | size_t gfx_init(FlutterEngine _engine) 750 | { 751 | engine = _engine; 752 | platform_thread_id = pthread_self(); 753 | 754 | // initialize display 755 | debug("initializing display..."); 756 | if (init_display() < 0) { 757 | error("init_display failed"); 758 | return EXIT_FAILURE; 759 | } 760 | return EXIT_SUCCESS; 761 | } 762 | 763 | void gfx_loop() 764 | { 765 | debug("Initializing Input devices..."); 766 | if (init_io() < 0) { 767 | error("init_io failed"); 768 | return; 769 | } 770 | 771 | // read input events 772 | debug("Running IO thread..."); 773 | if(run_io_thread() < 0) { 774 | error("run_io_thread failed"); 775 | return; 776 | } 777 | 778 | // run message loop 779 | debug("Running message loop..."); 780 | run_message_loop(); 781 | } 782 | 783 | void gfx_terminate(void) 784 | { 785 | debug("gfx_terminate not yet implemented"); 786 | } 787 | 788 | static void _post_platform_task(struct engine_task *task) 789 | { 790 | debug("_post_platform_task start"); 791 | struct engine_task *to_insert = malloc(sizeof(struct engine_task)); 792 | if (!to_insert) return; 793 | 794 | memcpy(to_insert, task, sizeof(struct engine_task)); 795 | pthread_mutex_lock(&tasklist_lock); 796 | if (tasklist == NULL || to_insert->target_time < tasklist->target_time) { 797 | to_insert->next = tasklist; 798 | tasklist = to_insert; 799 | } else { 800 | struct engine_task *prev = tasklist; 801 | struct engine_task *current = tasklist->next; 802 | while (current != NULL && to_insert->target_time > current->target_time) { 803 | prev = current; 804 | current = current->next; 805 | } 806 | to_insert->next = current; 807 | prev->next = to_insert; 808 | } 809 | 810 | pthread_mutex_unlock(&tasklist_lock); 811 | pthread_cond_signal(&task_added); 812 | debug("_post_platform_task end"); 813 | } 814 | 815 | static void _cut_word_from_string(char *string, char *word) 816 | { 817 | size_t word_length = strlen(word); 818 | char *word_in_str = strstr(string, word); 819 | 820 | // check if the given word is surrounded by spaces in the string 821 | if (word_in_str 822 | && ((word_in_str == string) || (word_in_str[-1] == ' ')) 823 | && ((word_in_str[word_length] == 0) || (word_in_str[word_length] == ' ')) 824 | ) { 825 | if (word_in_str[word_length] == ' ') word_length++; 826 | 827 | int i = 0; 828 | do { 829 | word_in_str[i] = word_in_str[i + word_length]; 830 | } while (word_in_str[i++ + word_length] != 0); 831 | } 832 | } 833 | 834 | static const GLubyte *_hacked_glGetString(GLenum name) 835 | { 836 | static GLubyte *extensions = NULL; 837 | 838 | if (name != GL_EXTENSIONS) 839 | return glGetString(name); 840 | 841 | if (extensions == NULL) { 842 | GLubyte *orig_extensions = (GLubyte *) glGetString(GL_EXTENSIONS); 843 | 844 | extensions = malloc(strlen((const char *)orig_extensions) + 1); 845 | if (!extensions) { 846 | debug("Could not allocate memory for modified GL_EXTENSIONS string"); 847 | return NULL; 848 | } 849 | 850 | strcpy((char *)extensions, (const char *)orig_extensions); 851 | 852 | /* 853 | * working (apparently) 854 | */ 855 | //_cut_word_from_string(extensions, "GL_EXT_blend_minmax"); 856 | //_cut_word_from_string(extensions, "GL_EXT_multi_draw_arrays"); 857 | //_cut_word_from_string(extensions, "GL_EXT_texture_format_BGRA8888"); 858 | //_cut_word_from_string(extensions, "GL_OES_compressed_ETC1_RGB8_texture"); 859 | //_cut_word_from_string(extensions, "GL_OES_depth24"); 860 | //_cut_word_from_string(extensions, "GL_OES_texture_npot"); 861 | //_cut_word_from_string(extensions, "GL_OES_vertex_half_float"); 862 | //_cut_word_from_string(extensions, "GL_OES_EGL_image"); 863 | //_cut_word_from_string(extensions, "GL_OES_depth_texture"); 864 | //_cut_word_from_string(extensions, "GL_AMD_performance_monitor"); 865 | //_cut_word_from_string(extensions, "GL_OES_EGL_image_external"); 866 | //_cut_word_from_string(extensions, "GL_EXT_occlusion_query_boolean"); 867 | //_cut_word_from_string(extensions, "GL_KHR_texture_compression_astc_ldr"); 868 | //_cut_word_from_string(extensions, "GL_EXT_compressed_ETC1_RGB8_sub_texture"); 869 | //_cut_word_from_string(extensions, "GL_EXT_draw_elements_base_vertex"); 870 | //_cut_word_from_string(extensions, "GL_EXT_texture_border_clamp"); 871 | //_cut_word_from_string(extensions, "GL_OES_draw_elements_base_vertex"); 872 | //_cut_word_from_string(extensions, "GL_OES_texture_border_clamp"); 873 | //_cut_word_from_string(extensions, "GL_KHR_texture_compression_astc_sliced_3d"); 874 | //_cut_word_from_string(extensions, "GL_MESA_tile_raster_order"); 875 | 876 | /* 877 | * should be working, but isn't 878 | */ 879 | _cut_word_from_string((char *)extensions, "GL_EXT_map_buffer_range"); 880 | 881 | /* 882 | * definitely broken 883 | */ 884 | _cut_word_from_string((char *)extensions, "GL_OES_element_index_uint"); 885 | _cut_word_from_string((char *)extensions, "GL_OES_fbo_render_mipmap"); 886 | _cut_word_from_string((char *)extensions, "GL_OES_mapbuffer"); 887 | _cut_word_from_string((char *)extensions, "GL_OES_rgb8_rgba8"); 888 | _cut_word_from_string((char *)extensions, "GL_OES_stencil8"); 889 | _cut_word_from_string((char *)extensions, "GL_OES_texture_3D"); 890 | _cut_word_from_string((char *)extensions, "GL_OES_packed_depth_stencil"); 891 | _cut_word_from_string((char *)extensions, "GL_OES_get_program_binary"); 892 | _cut_word_from_string((char *)extensions, "GL_APPLE_texture_max_level"); 893 | _cut_word_from_string((char *)extensions, "GL_EXT_discard_framebuffer"); 894 | _cut_word_from_string((char *)extensions, "GL_EXT_read_format_bgra"); 895 | _cut_word_from_string((char *)extensions, "GL_EXT_frag_depth"); 896 | _cut_word_from_string((char *)extensions, "GL_NV_fbo_color_attachments"); 897 | _cut_word_from_string((char *)extensions, "GL_OES_EGL_sync"); 898 | _cut_word_from_string((char *)extensions, "GL_OES_vertex_array_object"); 899 | _cut_word_from_string((char *)extensions, "GL_EXT_unpack_subimage"); 900 | _cut_word_from_string((char *)extensions, "GL_NV_draw_buffers"); 901 | _cut_word_from_string((char *)extensions, "GL_NV_read_buffer"); 902 | _cut_word_from_string((char *)extensions, "GL_NV_read_depth"); 903 | _cut_word_from_string((char *)extensions, "GL_NV_read_depth_stencil"); 904 | _cut_word_from_string((char *)extensions, "GL_NV_read_stencil"); 905 | _cut_word_from_string((char *)extensions, "GL_EXT_draw_buffers"); 906 | _cut_word_from_string((char *)extensions, "GL_KHR_debug"); 907 | _cut_word_from_string((char *)extensions, "GL_OES_required_internalformat"); 908 | _cut_word_from_string((char *)extensions, "GL_OES_surfaceless_context"); 909 | _cut_word_from_string((char *)extensions, "GL_EXT_separate_shader_objects"); 910 | _cut_word_from_string((char *)extensions, "GL_KHR_context_flush_control"); 911 | _cut_word_from_string((char *)extensions, "GL_KHR_no_error"); 912 | _cut_word_from_string((char *)extensions, "GL_KHR_parallel_shader_compile"); 913 | } 914 | 915 | return extensions; 916 | } 917 | 918 | void *proc_resolver(void *userdata, const char *name) 919 | { 920 | static int is_VC4 = -1; 921 | void *address; 922 | 923 | /* 924 | * The mesa V3D driver reports some OpenGL ES extensions as supported and working 925 | * even though they aren't. _hacked_glGetString is a workaround for this, which will 926 | * cut out the non-working extensions from the list of supported extensions. 927 | */ 928 | 929 | if (name == NULL) 930 | return NULL; 931 | 932 | // first detect if we're running on a VideoCore 4 / using the VC4 driver. 933 | if ((is_VC4 == -1) && (is_VC4 = strcmp(egl.renderer, "VC4 V3D 2.1") == 0)) 934 | printf( "detected VideoCore IV as underlying graphics chip, and VC4 as the driver.\n" 935 | "Reporting modified GL_EXTENSIONS string that doesn't contain non-working extensions."); 936 | 937 | // if we do, and the symbol to resolve is glGetString, we return our _hacked_glGetString. 938 | if (is_VC4 && (strcmp(name, "glGetString") == 0)) 939 | return _hacked_glGetString; 940 | 941 | if ((address = dlsym(RTLD_DEFAULT, name)) || (address = eglGetProcAddress(name))) 942 | return address; 943 | 944 | debug("proc_resolver: could not resolve symbol \"%s\"\n", name); 945 | 946 | return NULL; 947 | } -------------------------------------------------------------------------------- /src/embedder_gfx.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_EMBEDDER_GFX_H 2 | #define FLUTTER_EMBEDDER_GFX_H 3 | 4 | #include 5 | #include "flutter_embedder.h" 6 | 7 | /** 8 | * Initialize whatever graphics backend is configured. 9 | * This is where `FlutterEngineRun()` should be called. 10 | * 11 | * @param args partially complete (and already allocated) structure to pass to the engine 12 | * @return integer where 0 is ok, anything else is an error 13 | */ 14 | size_t gfx_init(FlutterEngine); 15 | 16 | bool gfx_make_current(void*); 17 | bool gfx_clear_current(void*); 18 | bool gfx_present(void*); 19 | uint32_t gfx_fbo_callback(void*); 20 | bool runs_platform_tasks_on_current_thread(void *); 21 | void on_post_flutter_task(FlutterTask, uint64_t, void *); 22 | void gfx_vsync(void *,intptr_t); 23 | void *proc_resolver(void *, const char *); 24 | 25 | /** 26 | * Cleanup the graphics backend 27 | */ 28 | size_t gfx_terminate(); 29 | 30 | /** 31 | * Main graphics backend loop 32 | */ 33 | void gfx_loop(); 34 | 35 | #endif -------------------------------------------------------------------------------- /src/embedder_glfw.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "embedder_gfx.h" 9 | #include "flutter_embedder.h" 10 | #include "debug.h" 11 | 12 | // This value is calculated after the window is created. 13 | static double g_pixelRatio = 1.0; 14 | static const size_t kInitialWindowWidth = 800; 15 | static const size_t kInitialWindowHeight = 600; 16 | GLFWwindow* window; 17 | 18 | void GLFWcursorPositionCallbackAtPhase(GLFWwindow *window, FlutterPointerPhase phase, double x, double y) 19 | { 20 | FlutterPointerEvent event = {}; 21 | event.struct_size = sizeof(event); 22 | event.phase = phase; 23 | event.x = x * g_pixelRatio; 24 | event.y = y * g_pixelRatio; 25 | event.timestamp = FlutterEngineGetCurrentTime(); 26 | FlutterEngineSendPointerEvent(glfwGetWindowUserPointer(window), &event, 1); 27 | } 28 | 29 | void GLFWcursorPositionCallback(GLFWwindow *window, double x, double y) 30 | { 31 | GLFWcursorPositionCallbackAtPhase(window, kMove, x, y); 32 | } 33 | 34 | void GLFWmouseButtonCallback(GLFWwindow *window, int key, int action, int mods) 35 | { 36 | if (key == GLFW_MOUSE_BUTTON_1 && action == GLFW_PRESS) { 37 | double x, y; 38 | glfwGetCursorPos(window, &x, &y); 39 | GLFWcursorPositionCallbackAtPhase(window, kDown, x, y); 40 | glfwSetCursorPosCallback(window, GLFWcursorPositionCallback); 41 | } 42 | 43 | if (key == GLFW_MOUSE_BUTTON_1 && action == GLFW_RELEASE) { 44 | double x, y; 45 | glfwGetCursorPos(window, &x, &y); 46 | GLFWcursorPositionCallbackAtPhase(window, kUp, x, y); 47 | glfwSetCursorPosCallback(window, NULL); 48 | } 49 | } 50 | 51 | void GLFWKeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) 52 | { 53 | if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { 54 | glfwSetWindowShouldClose(window, GLFW_TRUE); 55 | } 56 | } 57 | 58 | void GLFWwindowSizeCallback(GLFWwindow *window, int width, int height) 59 | { 60 | FlutterWindowMetricsEvent event = {}; 61 | event.struct_size = sizeof(event); 62 | event.width = width * g_pixelRatio; 63 | event.height = height * g_pixelRatio; 64 | event.pixel_ratio = g_pixelRatio; 65 | FlutterEngineSendWindowMetricsEvent(glfwGetWindowUserPointer(window), &event); 66 | } 67 | 68 | bool runs_platform_tasks_on_current_thread(void *userdata) 69 | { 70 | return true; 71 | } 72 | 73 | void on_post_flutter_task( 74 | FlutterTask task, 75 | uint64_t target_time, 76 | void* userdata 77 | ) 78 | { 79 | FlutterEngineRunTask(glfwGetWindowUserPointer(window), &task); 80 | return; 81 | } 82 | 83 | bool gfx_make_current(void *userdata) 84 | { 85 | glfwMakeContextCurrent(window); 86 | glewInit(); 87 | return true; 88 | } 89 | 90 | bool gfx_clear_current(void *userdata) 91 | { 92 | glfwMakeContextCurrent(window); 93 | return true; 94 | } 95 | 96 | bool gfx_present(void *userdata) 97 | { 98 | glfwSwapBuffers(window); 99 | return true; 100 | } 101 | 102 | uint32_t gfx_fbo_callback(void *userdata) 103 | { 104 | return 0; 105 | } 106 | 107 | /** 108 | * elixir embedder "callbacks" 109 | */ 110 | 111 | size_t gfx_init(FlutterEngine engine) 112 | { 113 | int result; 114 | result = glfwInit(); 115 | if (result != GLFW_TRUE) 116 | return result; 117 | 118 | window = glfwCreateWindow(kInitialWindowWidth, kInitialWindowHeight, "Flutter", NULL, NULL); 119 | if (!window) 120 | return -1; 121 | 122 | int framebuffer_width, framebuffer_height; 123 | glfwGetFramebufferSize(window, &framebuffer_width, &framebuffer_height); 124 | g_pixelRatio = framebuffer_width / kInitialWindowWidth; 125 | 126 | 127 | glfwSetWindowUserPointer(window, engine); 128 | GLFWwindowSizeCallback(window, kInitialWindowWidth, kInitialWindowHeight); 129 | glfwSetKeyCallback(window, GLFWKeyCallback); 130 | glfwSetWindowSizeCallback(window, GLFWwindowSizeCallback); 131 | glfwSetMouseButtonCallback(window, GLFWmouseButtonCallback); 132 | return 0; 133 | } 134 | 135 | size_t gfx_terminate() 136 | { 137 | glfwDestroyWindow(window); 138 | glfwTerminate(); 139 | } 140 | 141 | void gfx_loop() 142 | { 143 | while (!glfwWindowShouldClose(window)) { 144 | glfwWaitEventsTimeout(0.1); 145 | } 146 | } -------------------------------------------------------------------------------- /src/embedder_platform_message.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "erlcmd.h" 4 | #include "embedder_platform_message.h" 5 | #include "flutter_embedder.h" 6 | 7 | /** 8 | * Initialize queue and pthread lock 9 | * @param queue the queue 10 | */ 11 | size_t plat_msg_queue_init(plat_msg_queue_t *queue) 12 | { 13 | memset(queue, 0, sizeof(plat_msg_queue_t)); 14 | queue->messages = NULL; 15 | queue->index = 1; 16 | if (pthread_mutex_init(&queue->lock, NULL) != 0) { 17 | return 1; 18 | } 19 | return 0; 20 | } 21 | 22 | /** 23 | * @param message message data that came from `on_platform_message` Flutter callback 24 | */ 25 | plat_msg_container_t *plat_msg_push(plat_msg_queue_t *queue, const FlutterPlatformMessage *message) 26 | { 27 | pthread_mutex_lock(&queue->lock); 28 | plat_msg_container_t *new = malloc(sizeof(plat_msg_container_t)); 29 | if (!new) 30 | return NULL; 31 | 32 | new->message_size = message->message_size; 33 | new->message = (const uint8_t *)malloc(message->message_size); 34 | if (!new->message_size) { 35 | free(new); 36 | return NULL; 37 | } 38 | memset((uint8_t *)new->message, 0, message->message_size); 39 | memcpy((uint8_t *)new->message, message->message, message->message_size); 40 | 41 | new->channel_size = strlen(message->channel); 42 | new->channel = (const char *)malloc(new->channel_size); 43 | if (!new->channel) { 44 | free((char *)new->message); 45 | free(new); 46 | return NULL; 47 | } 48 | memset((char *)new->channel, 0, new->channel_size); 49 | memcpy((char *)new->channel, message->channel, new->channel_size); 50 | new->response_handle = message->response_handle; 51 | new->dispatched = false; 52 | new->cookie = queue->index++; 53 | new->next = queue->messages; 54 | queue->messages = new; 55 | 56 | pthread_mutex_unlock(&queue->lock); 57 | return new; 58 | } 59 | 60 | void plat_msg_process(plat_msg_queue_t *queue, 61 | FlutterEngine engine, 62 | const uint8_t *buffer, 63 | size_t length) 64 | { 65 | 66 | pthread_mutex_lock(&queue->lock); 67 | // I didn't come up w/ this. Uses double pointer to slice out nodes without 68 | // having to track current, previous and head all at the same time 69 | // https://codereview.stackexchange.com/posts/539/revisions 70 | for (plat_msg_container_t **current = &queue->messages; *current; current = &(*current)->next) { 71 | if ((*current)->cookie == buffer[sizeof(uint32_t)]) { 72 | // I have no idea if this is correct. there are little docs for it. 73 | // What docs to exist say `FlutterEngineSendPlatformMessageResponse` must ALWAYS 74 | // be called, but i'm not really sure what to call it with if the 75 | // platform message has no response. 76 | // FlutterPi doesn't call anything if there's no listener. 77 | FlutterEngineSendPlatformMessageResponse(engine, 78 | (*current)->response_handle, 79 | buffer + sizeof(uint32_t) +1, 80 | length - sizeof(uint8_t)); 81 | plat_msg_container_t *next = (*current)->next; 82 | free((uint8_t *)(*current)->message); 83 | free((char *)(*current)->channel); 84 | (*current)->message = NULL; 85 | (*current)->channel = NULL; 86 | free(*current); 87 | *current = next; 88 | goto cleanup; 89 | } 90 | } 91 | goto cleanup; 92 | cleanup: 93 | pthread_mutex_unlock(&queue->lock); 94 | return; 95 | } 96 | 97 | size_t plat_msg_dispatch_all(plat_msg_queue_t *queue, struct erlcmd *handler) 98 | { 99 | pthread_mutex_lock(&queue->lock); 100 | plat_msg_container_t *current = queue->messages; 101 | size_t r = 0; 102 | while (current) { 103 | if (!current->dispatched) { 104 | r = plat_msg_dispatch(current, handler); 105 | if (r < 0) { 106 | r = -1; 107 | goto cleanup; 108 | } 109 | } 110 | current = current->next; 111 | } 112 | cleanup: 113 | pthread_mutex_unlock(&queue->lock); 114 | return r; 115 | } 116 | 117 | size_t plat_msg_dispatch(plat_msg_container_t *container, struct erlcmd *handler) 118 | { 119 | if (container->dispatched) 120 | return -3; 121 | 122 | if (container->message_size > 0xff) 123 | return -2; 124 | 125 | size_t buffer_length = sizeof(uint32_t) + // erlcmd length packet 126 | sizeof(uint8_t) + // opcode 127 | sizeof(uint8_t) + // handle 128 | sizeof(uint16_t) + // channel_length 129 | container->channel_size + // channel 130 | sizeof(uint16_t) + // message_length 131 | container->message_size; 132 | uint8_t *buffer; 133 | buffer = malloc(buffer_length); 134 | if (!buffer) 135 | return -1; 136 | 137 | memset(buffer, 0, buffer_length); 138 | 139 | buffer[sizeof(uint32_t)] = 0x0; 140 | buffer[sizeof(uint32_t) +1] = container->cookie; 141 | memcpy(buffer + sizeof(uint32_t) +2, &container->channel_size, sizeof(uint16_t)); 142 | memcpy(buffer + sizeof(uint32_t) +4, container->channel, container->channel_size); 143 | memcpy(buffer + sizeof(uint32_t) +4 + container->channel_size, &container->message_size, sizeof(uint16_t)); 144 | memcpy(buffer + sizeof(uint32_t) +4 + container->channel_size + sizeof(uint16_t), container->message, 145 | container->message_size); 146 | 147 | erlcmd_send(handler, buffer, buffer_length); 148 | container->dispatched = true; 149 | 150 | free(buffer); 151 | return 0; 152 | } 153 | 154 | size_t plat_msg_queue_destroy(plat_msg_queue_t *queue) 155 | { 156 | return pthread_mutex_destroy(&queue->lock); 157 | } -------------------------------------------------------------------------------- /src/embedder_platform_message.h: -------------------------------------------------------------------------------- 1 | #ifndef EMBEDDER_PLATFORM_MESSAGE_H 2 | #define EMBEDDER_PLATFORM_MESSAGE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "flutter_embedder.h" 8 | #include "erlcmd.h" 9 | 10 | typedef struct platform_message 11 | { 12 | uint8_t cookie; 13 | bool dispatched; 14 | struct platform_message *next; 15 | 16 | // All of these came from FlutterPlatformMessageResponseHandle 17 | const char* channel; 18 | const uint8_t* message; 19 | size_t channel_size; 20 | size_t message_size; 21 | const FlutterPlatformMessageResponseHandle* response_handle; 22 | } plat_msg_container_t; 23 | 24 | typedef struct platform_message_queue 25 | { 26 | plat_msg_container_t* messages; 27 | pthread_mutex_t lock; 28 | uint32_t index; 29 | } plat_msg_queue_t; 30 | 31 | size_t plat_msg_queue_init(plat_msg_queue_t*); 32 | plat_msg_container_t* plat_msg_push(plat_msg_queue_t*, const FlutterPlatformMessage*); 33 | void plat_msg_process(plat_msg_queue_t*, FlutterEngine, const uint8_t*, size_t); 34 | size_t plat_msg_dispatch_all(plat_msg_queue_t*, struct erlcmd*); 35 | size_t plat_msg_dispatch(plat_msg_container_t*, struct erlcmd*); 36 | size_t plat_msg_queue_destroy(plat_msg_queue_t*); 37 | 38 | #endif -------------------------------------------------------------------------------- /src/erlcmd.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frank Hunleth 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Common Erlang->C port communications code 17 | */ 18 | 19 | #include "erlcmd.h" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | 29 | /** 30 | * Initialize an Erlang command handler. 31 | * 32 | * @param handler the structure to initialize 33 | * @param read_fd the file descriptor to read() 34 | * @param write_fd the file descriptor to write() 35 | * @param request_handler callback for each message received 36 | * @param cookie optional data to pass back to the handler 37 | */ 38 | void erlcmd_init(struct erlcmd *handler, int read_fd, int write_fd, 39 | void (*request_handler)(const uint8_t *req, size_t length, void *cookie), 40 | void *cookie) 41 | { 42 | memset(handler, 0, sizeof(*handler)); 43 | handler->read_fd = read_fd; 44 | handler->write_fd = write_fd; 45 | handler->request_handler = request_handler; 46 | handler->cookie = cookie; 47 | } 48 | 49 | /** 50 | * @brief Synchronously send a response back to Erlang 51 | * 52 | * The message to be sent back to Erlang should start at &response[2]. A 53 | * two-byte big endian length will be filled in by this function. 54 | * 55 | * @param response what to send back 56 | * @param len the total length of the message including the two-byte header 57 | */ 58 | void erlcmd_send(struct erlcmd *handler, uint8_t *response, size_t len) 59 | { 60 | uint32_t be_len = htonl(len - sizeof(uint32_t)); 61 | memcpy(response, &be_len, sizeof(be_len)); 62 | 63 | size_t wrote = 0; 64 | do { 65 | ssize_t amount_written = write(handler->write_fd, response + wrote, len - wrote); 66 | if (amount_written < 0) { 67 | if (errno == EINTR) 68 | continue; 69 | 70 | err(EXIT_FAILURE, "write"); 71 | } 72 | 73 | wrote += amount_written; 74 | } while (wrote < len); 75 | } 76 | 77 | /** 78 | * @brief Dispatch commands in the buffer 79 | * @return the number of bytes processed 80 | */ 81 | static size_t erlcmd_try_dispatch(struct erlcmd *handler) 82 | { 83 | /* Check for length field */ 84 | if (handler->index < sizeof(uint32_t)) 85 | return 0; 86 | 87 | uint32_t be_len; 88 | memcpy(&be_len, handler->buffer, sizeof(uint32_t)); 89 | size_t msglen = ntohl(be_len); 90 | if (msglen + sizeof(uint32_t) > sizeof(handler->buffer)) 91 | errx(EXIT_FAILURE, "Message too long"); 92 | 93 | /* Check whether we've received the entire message */ 94 | if (msglen + sizeof(uint32_t) > handler->index) 95 | return 0; 96 | 97 | handler->request_handler(handler->buffer, msglen, handler->cookie); 98 | 99 | return msglen + sizeof(uint32_t); 100 | } 101 | 102 | /** 103 | * @brief call to process any new requests from Erlang 104 | */ 105 | void erlcmd_process(struct erlcmd *handler) 106 | { 107 | ssize_t amount_read = read(handler->read_fd, handler->buffer + handler->index, 108 | sizeof(handler->buffer) - handler->index); 109 | if (amount_read < 0) { 110 | /* EINTR is ok to get, since we were interrupted by a signal. */ 111 | if (errno == EINTR) 112 | return; 113 | 114 | /* Everything else is unexpected. */ 115 | err(EXIT_FAILURE, "read"); 116 | } else if (amount_read == 0) { 117 | /* EOF. Erlang process was terminated. This happens after a release or if there was an error. */ 118 | exit(EXIT_SUCCESS); 119 | } 120 | 121 | handler->index += amount_read; 122 | for (;;) { 123 | size_t bytes_processed = erlcmd_try_dispatch(handler); 124 | 125 | if (bytes_processed == 0) { 126 | /* Only have part of the command to process. */ 127 | break; 128 | } else if (handler->index > bytes_processed) { 129 | /* Processed the command and there's more data. */ 130 | memmove(handler->buffer, &handler->buffer[bytes_processed], handler->index - bytes_processed); 131 | handler->index -= bytes_processed; 132 | } else { 133 | /* Processed the whole buffer. */ 134 | handler->index = 0; 135 | break; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/erlcmd.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frank Hunleth 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * Common Erlang->C port communications declarations 17 | */ 18 | 19 | #ifndef ERLCMD_H 20 | #define ERLCMD_H 21 | 22 | #include 23 | #include 24 | 25 | /* 26 | * Erlang request/response processing 27 | * 28 | * The buffer needs to be large enough to hold the biggest individual 29 | * message from Erlang. 30 | */ 31 | #define ERLCMD_BUF_SIZE (8192) 32 | struct erlcmd { 33 | uint8_t buffer[ERLCMD_BUF_SIZE]; 34 | size_t index; 35 | 36 | void (*request_handler)(const uint8_t *buffer, size_t length, void *cookie); 37 | void *cookie; 38 | int read_fd; 39 | int write_fd; 40 | }; 41 | 42 | void erlcmd_init(struct erlcmd *handler, int read_fd, int write_fd, 43 | void (*request_handler)(const uint8_t *req, size_t length, void *cookie), 44 | void *cookie); 45 | void erlcmd_send(struct erlcmd *handler, uint8_t *response, size_t len); 46 | void erlcmd_process(struct erlcmd *handler); 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/precompiled_engine_aarch64.mk: -------------------------------------------------------------------------------- 1 | ZIPFILE=linux-aarch64-embedder.zip 2 | FLUTTER_PI_VERSION=341288caed5ef3450ed545e196733fee0cf6a568 3 | FLUTTER_PI_URL=https://github.com/ardera/flutter-pi/archive/$(FLUTTER_PI_VERSION).zip 4 | 5 | $(PREFIX)/libflutter_engine.so: $(ZIPFILE) 6 | unzip -p $(ZIPFILE) flutter-pi-$(FLUTTER_PI_VERSION)/arm64/libflutter_engine.so.debug > $(PREFIX)/libflutter_engine.so 7 | 8 | flutter_embedder.h: $(ZIPFILE) 9 | unzip -p $(ZIPFILE) flutter-pi-$(FLUTTER_PI_VERSION)/flutter_embedder.h > flutter_embedder.h 10 | 11 | $(PREFIX)/icudtl.dat: 12 | unzip -p $(ZIPFILE) flutter-pi-$(FLUTTER_PI_VERSION)/arm64/icudtl.dat > $(PREFIX)/icudtl.dat 13 | 14 | $(ZIPFILE): 15 | wget -O $(ZIPFILE) $(FLUTTER_PI_URL) -------------------------------------------------------------------------------- /src/precompiled_engine_armv7.mk: -------------------------------------------------------------------------------- 1 | ZIPFILE=linux-arm-embedder.zip 2 | FLUTTER_PI_VERSION=341288caed5ef3450ed545e196733fee0cf6a568 3 | FLUTTER_PI_URL=https://github.com/ardera/flutter-pi/archive/$(FLUTTER_PI_VERSION).zip 4 | 5 | $(PREFIX)/libflutter_engine.so: $(ZIPFILE) 6 | unzip -p $(ZIPFILE) flutter-pi-$(FLUTTER_PI_VERSION)/arm/libflutter_engine.so.debug > $(PREFIX)/libflutter_engine.so 7 | 8 | flutter_embedder.h: $(ZIPFILE) 9 | unzip -p $(ZIPFILE) flutter-pi-$(FLUTTER_PI_VERSION)/flutter_embedder.h > flutter_embedder.h 10 | 11 | $(PREFIX)/icudtl.dat: 12 | unzip -p $(ZIPFILE) flutter-pi-$(FLUTTER_PI_VERSION)/arm/icudtl.dat > $(PREFIX)/icudtl.dat 13 | 14 | $(ZIPFILE): 15 | wget -O $(ZIPFILE) $(FLUTTER_PI_URL) -------------------------------------------------------------------------------- /src/precompiled_engine_x86_64.mk: -------------------------------------------------------------------------------- 1 | ZIPFILE=linux-x64-embedder.zip 2 | FLUTTER_ENGINE_VERSION = 2c956a31c0a3d350827aee6c56bb63337c5b4e6e 3 | 4 | $(PREFIX)/libflutter_engine.so: $(ZIPFILE) 5 | unzip -p $(ZIPFILE) libflutter_engine.so > $(PREFIX)/libflutter_engine.so 6 | 7 | flutter_embedder.h: $(ZIPFILE) 8 | unzip -p $(ZIPFILE) flutter_embedder.h > flutter_embedder.h 9 | 10 | $(PREFIX)/icudtl.dat: 11 | cp ../icudtl.dat $(PREFIX)/icudtl.dat 12 | 13 | $(ZIPFILE): 14 | wget -q -O $(ZIPFILE) https://storage.googleapis.com/flutter_infra/flutter/$(FLUTTER_ENGINE_VERSION)/linux-x64/linux-x64-embedder --------------------------------------------------------------------------------