Skip to content

Add Larod #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 77 commits into
base: main
Choose a base branch
from
Open

Add Larod #120

wants to merge 77 commits into from

Conversation

JsGjKJzi
Copy link
Contributor

The acap-rs workspace doesn't currently provide wrappers for the larod library. When it is ready, I hope this merge request will provide that.

Immediate goals:

  • Provide safe Rust wrappers around the Larod functions, and abstract into rust structs and impls where appropriate.
  • Provide tests to verify functionality.

Since larod uses peripheral hardware, I plan to build the tests and execute them on an Axis P1468-LE.

Checklist before requesting a review

  • I have performed a self-review of my own code
  • I have verified that the code builds perfectly fine on my local system
  • I have added tests that prove my fix is effective or that my feature works
  • I have commented my code, particularly in hard-to-understand areas
  • I have verified that my code follows the style already available in the repository
  • I have made corresponding changes to the documentation

@JsGjKJzi
Copy link
Contributor Author

Just adding a comment that I am still relatively new to Rust, so suggestions to improve my code is very welcome. I will also be learning about functionality of the larod library during the process of creating this wrapper. So, guidance on interfacing with larod is also very welcome.

@JsGjKJzi
Copy link
Contributor Author

Looking at the larodError documentation a bit more closely, I realize what I have so far won't work, and maybe larod cannot be wrapped?

https://axiscommunications.github.io/acap-documentation/docs/api/src/api/larod/html/structlarodError.html

Notably larodError, which is passed to nearly every function, must be uninitialized. It might be impossible for rust to produce such a pointer https://doc.rust-lang.org/std/mem/union.MaybeUninit.html.

@apljungquist
Copy link
Collaborator

Since you say the code is not ready I will focus on answering your questions and assist you when you ask for it for now. Consequently I haven't looked much at the code but I address your comments below.


Just adding a comment that I am still relatively new to Rust

Diving in at the deep end with unsafe. A word of warning; In my opinion unsafe combines the worst of C(++) and Rust, and writing safe wrappers is especially frustrating since many of the safety aspects that we need to guarantee are not documented - sometimes not even considered - by the library authors. To make matters worse, we often cannot even test empirically if our code is correct because UB does not mean that it will segfault or do anything weird right now, it means it could do it now or at any point in the future. So, know know what you are getting yourself into, that it is ok to quit and don't let this experience discourage you from using Rust because there's another side to the language that is carefree and fun 🌞 🌈

What is your programming background? I will try to adjust my communication accordingly.

I will also be learning about functionality of the larod library during the process of creating this wrapper.

That makes two of us.

Notably larodError, which is passed to nearly every function, must be uninitialized.

I have found that sometimes the documentation should be taken with a grain of salt (I have found errors) and other times it is important to read it carefully. In any case the examples can be a good supporting resource. in vdo_larod.c 1 we find this:

larodError* error = NULL;
// ...
if (!larodConnect(&conn, &error)) {

We clearly don't pass an uninitialized variable but a pointer to null. I think this is what the documentation means when it says "An uninitialized handle to an error".

Compare this to send_event.c 2 which is makes heavy use of glib and, presumably, follows glib error conventions:

GError* error = NULL;
// ...
if (!ax_event_handler_declare(event_handler,
                                  // ...
                                  &error)) {

Footnotes

  1. https://github.com/AxisCommunications/acap-native-sdk-examples/blob/9b00b2fdf23672f8910421653706572201c2ed8b/vdo-larod/app/vdo_larod.c#L85

  2. https://github.com/AxisCommunications/acap-native-sdk-examples/blob/9b00b2fdf23672f8910421653706572201c2ed8b/axevent/send_event/app/send_event.c#L170

@JsGjKJzi
Copy link
Contributor Author

Thanks for the feedback. My background is varied engineering (R&D, so jack of all trades master of none sort of thing). I've written production C++ for embedded microcontrollers, C++ for AI using OpenCV and libtorch, lots of Python, and a few web backend type services using Rust. Again, I'm no expert, but I've done a lot of dabbling in various things.

I'm ok diving into the deep end. Just more to learn hopefully :).

One piece from the larod docs that I worry a bit about is this quote, "error can also be NULL if one does not want any error information." I worry that if I initialize the pointer to Null, we'll just never get any error information back.

@apljungquist
Copy link
Collaborator

I'm comparing to axevent again because I recently worked on those bindings.

I think this is what passing a null pointer would look like: https://github.com/AxisCommunications/acap-native-sdk-examples/blob/9b00b2fdf23672f8910421653706572201c2ed8b/axevent/send_event/app/send_event.c#L167

Compare that to passing a (not-null) pointer to a null pointer: https://github.com/AxisCommunications/acap-native-sdk-examples/blob/9b00b2fdf23672f8910421653706572201c2ed8b/axevent/send_event/app/send_event.c#L176

… the pointer to it is non-NULL.

- Added some documentation.
- Mimicked the try_from macro from the axevent crate to DRY the code.
- Stubbed out functions for Session (larodConnection)
- Start implementing a few functions to list hand handle larodDevices
- Add testing documentation to README.md
- Add runner specification to .cargo/config.toml (new)
- Set `aarch64-unknown-linux-gnu` to default build target.
- Add remote-test.sh script to run tests on remote target.
@JsGjKJzi
Copy link
Contributor Author

JsGjKJzi commented Nov 2, 2024

@apljungquist I just set up the P1468-LE camera and set up a remote testing workflow. I've added some documentation and configuration files in this commit.

It might be useful for you in your endeavors. I have confirmed it works for the larod library so far on the P1468-LE running OS 11.11.73. It might be worth considering as a cherry pick, or I could open a separate merge request for it.

Finishedtest` profile [unoptimized + debuginfo] target(s) in 1.15s
Running unittests src/lib.rs (/workspaces/acap-rs/target/aarch64-unknown-linux-gnu/debug/deps/larod-d753965a4827e400)
larod-d753965a4827e400 100% 6363KB 9.9MB/s 00:00

running 10 tests
test tests::it_creates_larod_map ... ok
test tests::larod_map_can_get_2_tuple ... ok
test tests::it_drops_map ... ok
test tests::larod_map_can_get_4_tuple ... ok
test tests::larod_map_can_get_str ... ok
test tests::larod_map_can_get_int ... ok
test tests::larod_map_can_set_int ... ok
test tests::larod_map_can_set_4_tuple ... ok
test tests::larod_map_can_set_2_tuple ... ok
test tests::larod_map_can_set_str ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
`

@apljungquist
Copy link
Collaborator

apljungquist commented Nov 2, 2024

Neat. I didn’t know about runner so I implemented something similar in cargo-asap-sdk. I’m traveling over the weekend but when I’m back I’ll try out your solution and think about how we could leverage it.

I currently think of tests in two categories:

  • Library test. This is what you have written and implemented support. We typically upload them to /tmp/ and run them as root. Starting with AXIS OS 12.0 this will be difficult or impossible to do
  • Application tests. These can be packaged as an EAP and installed on the device as any app. They run as the same user as the app would, which is required for APIs like message broker. This likely will continue to work or, in the worst case, the test output would have to be retrieved from the system logs.

Were you aware of cargo-acap-sdk test? Asking because if not then I have a marketing problem.

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the simplicity of this script but before I make any suggestions about how to upstream it I have a couple of questions to make sure I understand it.

- Change error handling to better check for a non-NULL larodError pointer.
- Add some documentation for LarodMap functions.
- Set LarodMap functions to public.
- Add some additional tests.
- Hide on-device tests behind a device-tests feature.
- Add a basic example.
@JsGjKJzi
Copy link
Contributor Author

JsGjKJzi commented Nov 6, 2024

Were you aware of cargo-acap-sdk test? Asking because if not then I have a marketing problem.
I might have been, but have been caught up in thinking through the API I'd like to achieve that I may have forgot about it. I have definitely used cargo-acap-sdk build to build an eap.

Using that may ultimately be a better approach than using the runner as discussed above.

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to see you are still making progress. I dropped a couple of comments on superficial stuff that came to mind while skimming. Do with them as you please.

@@ -589,7 +601,29 @@ impl<'a> Tensor<'a> {
Err(maybe_error.unwrap_or(Error::MissingLarodError))
}
}
pub fn set_pitches() {}
pub fn set_pitches(&mut self, pitches: &[usize]) -> Result<()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you know you can make a function generic over N?

I haven't looked closely at what we are trying to do here, so I can't say if it would help or be wise, just some basic pattern matching going on in my brain.

https://doc.rust-lang.org/reference/items/generics.html#const-generics

P: AsRef<Path>,
{
let f = File::open(model_file).map_err(Error::IOError)?;
let Ok(device_name) = CString::new(<(M, H)>::as_str()) else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning to use the result of as_str for anything other than creating a CString?
If not you may be interested to know that there are c-string literals these days.

@JsGjKJzi
Copy link
Contributor Author

@apljungquist In general this crate is "done" in the sense that the vast majority of the functions and functionality exist. I have also documented most things. I didn't go into as much detail as the official documentation, but included a link to the Axis ACAP page. I guess I'll mark it as ready for review and I can address any shortcomings.

There are still a few things I'm not keen on, and a few that I'm not sure how best to implement.

Ergonomics

First, because the type implementing LarodModel owns its input and output tensors, and because we need to get a mutable reference to them to call copy_from_slice, we have to go through a dance to use them. It is particularly unsightly when running in a for loop, like looping over frames from VDO.

for buffer in running_stream.iter() {
    if let Some(ft) = preprocessor.input_tensors_mut().and_then(|it| it.first_mut()) {
        ft.copy_from_slice(frame_slice);
        job_request.run()?
    }
}

I was hopeful for something like this:

let preproc_tensor_iter = preprocessor.input_tensors_mut()?.iter_mut()?;
let preproc_input_tensor = preproc_tensor_iter.next()?
for buffer in running_stream.iter() {
  ...
}

But then, since preproc_input_tensor holds a mutable reference to the preprocessor, we can't easily access the output tensors.

Access Methods

Second, the larod library has a larodModel* larodGetModel( larodConnection *conn, const uint64_t modelId, larodError **error ) function that returns a model. I'm generally not sure how or why this function would be used. Especially since one would need to get the modelId via something like uint64_t larodGetModelId( const larodModel *model, larodError **error ) which requires that one have a pointer to the model in the first place.

Nevertheless, if one were to call larodGetModel, since both a Preprocessor and InferenceModel implement the LarodModel trait, it would have to be a function that returns some dynamic trait via something like

fn get_model(model_id: u64) -> Box<dyn LarodModel> {
    if preprocessor_model {
        Box::new(Preprocessor {})
    } else {
        Box::new(InferenceModel {})
    }
}

This would be a particularly involved function if we need to map the file descriptors and everything back into an InferenceModel for example. I'm not keen on that, but it could be done if you think it is necessary.

@JsGjKJzi JsGjKJzi marked this pull request as ready for review February 25, 2025 15:24
@JsGjKJzi JsGjKJzi requested a review from a team as a code owner February 25, 2025 15:24
@apljungquist
Copy link
Collaborator

That's great news. I'll take a look at it this weekend.

It would be nice with an example app that demonstrates that it works and how to use it.

Copy link
Collaborator

@apljungquist apljungquist left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's cool that we are getting close to having larod bindings and appreciate your effort. The PR is on the big side and it would be nice to split it up somehow to make it easier to reason about and keep moving forward. One thing that seems reasonable to chip off is the sys-crate, so I've focused on that this morning. I'm thinking that we can discuss it in this PR and once we are happy we isolate it and merge. What do you think?

On the topic of how to review, do you have any requests of suggestions for me?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a camera at hand, but I guess these examples are meant to be executed using cargo run and relying on remote-test.sh for them to execute on the camera. Is that correct?
Would it be possible to package them as example apps?
Do you see any drawback to doing so?

Advantages in my opinion include:

  • It is consistent with the rest of the project where most programs that are intended to run on the camera are packaged as applications.
  • It communicates that the examples can run as normal applications with all the restrictions that entail, and shows how to set up a manifest that allows it.

@@ -0,0 +1,141 @@
//! # Simple example
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, detailed instructions. I look forward to trying them when I have a camera at hand.

.allowlist_recursively(false)
.allowlist_function("^(larod.*)$")
.allowlist_type("^(_?larod.*)$")
.default_enum_style(bindgen::EnumVariation::Rust {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should use Rust style for enums that are returned from foreign functions because it would be undefined behavior if the foreign function adds a new variant. What do you think?

#[allow(non_camel_case_types)]
#[repr(u32)]
#[derive(Debug)]
pub enum FDAccessFlag {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these types in the sys crate?

@JsGjKJzi JsGjKJzi mentioned this pull request Apr 20, 2025
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants