I can’t comment on the code organization much, as I haven’t done enough async coding to get a good feel for it. If I needed a library for one of my non-async projects, though, I’d think twice about pulling in a (presumably) heavyweight dependency like tokio.
That's one of my concerns as well. I'm only using the "rt-core" feature of tokio which is just 1 of like over 15 of tokio's features, but still I'm concerned if I should be using tokio here.
It was the only way (to my current knowledge) to use an async fn bar() from the fn foo()
You might want to look at the code base for reqwest. It offers an async and a blocking api. I believe that the blocking api don't require an async runtime to run. I only looked at the blocking code briefly a while back but from what I remember it runs async futures on an extremely simple blocking async executor. Maybe there is a small library that can do this for you instead of implementing it yourself like reqwest has.
I think @asafigan’s recommendation was to look at how reqwest structures its code, because it is doing the same thing you want to: providing both a sync and async api.
I haven't done this myself, but I would try Handle::try_current to see if a runtime already exists, and create a new one on error. This way you could (hopefully) avoid creating a new runtime if one already exists, avoiding the runtime inside runtime error.
I believe this is correct and it does use tokio under the hood even in the blocking API.
that seems interesting, looking into it. I'm not sure if I can use it in this case though. If I omit tokio completely from my end and use the blocking client of reqwest, reqwest would be creating one runtime, and the user of my lib would be creating another.... thus the error. I don't think I can intervene anywhere, unless I am misunderstanding what try_current does.
If you're going to be creating/getting a runtime anyway, I don't think there's any reason to use a blocking client, is there? I guess it might be due to something not visible in this example, but I think you should be able to structure your code so the blocking client isn't needed. I'm not much of an async expert myself, unfortunately, so I might be mistaken
Well I'm making a library, I need to let the user choose either or, Like what reqwest itself is doing. I can't tell the user to not use async if he needs to.
Right now I just decided to refactor most of my code to make both separately, and reuse as much code as possible across them. I'll share the thing here after I'm done.
tokio is a mandatory dependency of reqwest though, right? Even if you only use the blocking API you're depending on and using tokio in the background. I think it's a reasonable goal to be able to leave tokio out completely if the user doesn't need it, but you won't be able to do it while using reqwest, I don't think.
Generally the standard solution for making a blocking version of an api is to create a runtime and use block_on. It is typically recommended to provide something like reqwest's Client type to allow reuse of the runtime object.
I think using Tokio for the blocking api is perfectly fine, as Tokio is very light-weight if you only enable the basic scheduler. Note that you should construct the runtime through the builder to make sure that you are constructing a basic scheduler even if the user depends on the threaded scheduler feature of Tokio through some other dependency.
Don't bother using Handle::try_current. That method will pretty much only succeed inside async code, and blocking inside async code is a big no-no. If the user has a runtime, they can use your async api.