Skip to content

Commit

Permalink
NF: explaining the bridge between languages
Browse files Browse the repository at this point in the history
It was, honestly, quite confusing. And if I got it wrong, I guess this
code review will be the time for me to learn what was wrong.
  • Loading branch information
Arthur-Milchior committed Nov 11, 2024
1 parent 763712c commit 975e317
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CONTRIBUTORS
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ AMBOSS MD Inc. <https://www.amboss.com/>
Aristotelis P. <https://glutanimate.com/contact>
Erez Volk <[email protected]>
zjosua <[email protected]>
Arthur Milchior <[email protected]>
Yngve Hoiseth <[email protected]>
Arthur Milchior <[email protected]>
Ijgnd
Yoonchae Lee <[email protected]>
Evandro Coan <github.com/evandrocoan>
Expand Down
67 changes: 67 additions & 0 deletions docs/protobuf.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Protocol Buffers

ProtoBuf is a format used both to save data in storage and transmit
data between services. You can think of it as similar to JSON with
schemas, given that you can use basic types, list and records. Except
that it's usually represented in an efficient byteform and not in a
human readable way.

In this document we'll first explain how to encode and decode protobuf
messages in the three languages used in anki. Typescript, python and
rust. Please note that AnkiDroid and AnkiMobile are out of scope for
this document. In the second part, we'll explain how the code in those
three languages can communicate, usually using protobuf rpc.

# Reading and creating protobuf messages.

Anki uses [different implementations of Protocol Buffers](./architecture.md#protobuf)
and each has its own peculiarities. This document highlights some aspects relevant
to Anki and hopefully helps to avoid some common pitfalls.
Expand Down Expand Up @@ -116,3 +130,56 @@ Inside the `pb` module you will find all generated Rust types and their implemen
a valid variant, so the Rust code needs to deal with a lot of `Option`s. As we
don't expect other parts of Anki to send invalid messages, using an `InvalidInput`
error or `unwrap_or_default()` is usually fine.

# Communication between languages

As far as I understand, Rust, being the back-end, does not send
message to Python or Ts which are both front-ends. Instead, the front
end can request data, that Rust returns as part of a RPC initiated by
the front end. Those RPC are discussed below.

## Sending a message to typescript.

This is the only case that don't use protobuf. Any python code, having
access to a webview `wv` can send an arbitrary
`wv.web.eval(js_command)` or `wv.web.evalWithCallback(js _command,
cb)` to execute an arbitrary javascript command.

If your command need to be executed inside a svelte script, you can add it to `+page.svelte`, with

```js
globalThis.anki || = {};
globalThis.anki.methodName = function() {// or async
body
}
```

You then simply have to execute `anki.methodName()`.

Note that if the function is asynchronous, you can't directly send the
result to a callback. Instead your function will have to call a post
method that will be sent to python or rust. Which leads us to the next
section.

## From typescript to Python

In this section, I assume you want to send a rpc to python only, and not rust.

In this case:

- Add your rpc declaration `rpc NameOfMyRpc(InputType) returns (OutputType)` to [frontend.proto](/proto/anki/frontend.proto)'s FrontendService,
- In typescript you add `import {NameOfMyRpc} from "@generated/backend"` to use this RPC, you can then use `NameOfMyRpc(args)`,
- In [mediasrv.py](/qt/aqt/mediasrv.py) you create a method `def name_of_my_rpc(args) -> bytes`, and you add `name_of_my_rpc` to `post_handler_list`.

As you don't want to block the thread, unless the result can be really
quickly computed, you'll want to return `b""`, and use
`aqt.mw.taskman.run_on_main(function_doing_actual_work)`.

## From Typescript or Python to Rust

For the sake of the example, I'll consider that you want to add a RPC in the DecksService. Any service work similarly but `FrontendService`.

- In [decks.proto](/proto/anki/decks.proto)'s DecksService, you add your new rpc `rpc NameOfMyRpc(InputType) returns (OutputType)`.
- In [services.rs](/rslib/src/decks/service.rs)'s `impl crate::services::DecksService for Collection` you add the implementation `fn name_of_my_rpc(&mut self, args) -> Error::Result<OutputType>`
- In typescript you add `import {NameOfMyRpc} from "@generated/backend"` to use this RPC, you can then use `NameOfMyRpc(args)`,
- In Python you add can call `col._backend.name_of_my_rpc(args)`, with `col` being your collection object.

0 comments on commit 975e317

Please sign in to comment.