Cross Platform Mobile and Web Development with C++ Explained

Part 4: ReST Client

Image for post
Image for post

You can have piles of logic in your application but you need to get your data from somewhere at some point. Typically you will use a ReST API, this seems to have become the standard nowadays. I will show you how to implement your ReST client to produce objects that your view layer can use with minimal effort.

The source code is here: https://github.com/skonstant/xptuto/tree/part4_rest

ReST in C++

I took a while to search for a good portable HTTP library and I could not find any that meets my business requirements. These requirements are: Good quality HTTP, Auto-configured from the platform (SSL and Proxy, ) and Cache support.

As simple as it seems to be, there isn’t any C or C++ library that supports these. Libcurl is top notch, some people have made builds for iOS and Android but it qualifies only for the first of my requirement (yes, it does not even have an HTTP Cache implementation). If you find a package that could do the job I am willing to hear.

I found however that it did not matter much because our 3 target platforms do HTTP natively, their implementation will follow the platform configuration transparently and they have cache, with no effort. Just a little for Android, but nothing much.

You remember in the previous article, we “stubbed” our HTTP client? Now we will implement it 😇. We will write only GET and without extra headers, we leave it to the reader to imagine how to do the other methods.

Thin Platform Implementations

Android

For Android, even though Java has an HTTP implementation in the standard library, I find it cumbersome and it lacks the asynchronousness I need, so I typically go for OkHttp, it seems the de-facto standard for HTTP networking in Java these days. We remember we defined the HttpClient in Djinni, which gave us an abstract class, here is the implementation:

public class JavaHttpClient extends HttpClient {

private final OkHttpClient client;

JavaHttpClient(Context context) {
...
Cache cache = new Cache(httpCacheDirectory, cacheSize);

client = new OkHttpClient.Builder()
.cache(cache)
.build();
}

@Override
public void get(String url, final HttpCallback callback) {
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(new Callback() {
...
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) {
String body = "";
ResponseBody b = response.body();
if (b != null) {
try {
body = b.string();
} catch (IOException unused) {
}
}
callback.onResponse(body, response.code());
}
});
}
}

That was simple.

We start by configuring the cache, from the Cache directory of the app (the one that you can clear without losing the app data). And then we use the async enqueue() method where we then call back our (C++) callback.

This is as simple as that, the only thing to remember here is that body is copied to the C++ layer, so if your responses are big, you could get an OutOfMemory exception. I never seem to get any in Yelo Play even though I have some multi megabytes responses but you never know. This is not a C++ problem, it is the nature of a text API, unless you stream it into your parser, you need to have it in memory once.

One thing to note here, and this will be discussed in the next article, is that the enqueue() callback is called in another thread than the calling thread.

HTTP Cache is fully implemented and the SSL and Certificates policy of the device and of the app will be respected. You can try it with a debugging proxy like Charles, you will see the second request will get a 304 NOT MODIFIED response, when your code will get a 200 OK.

Last thing: onFailure() is only called in case of network error, not in case of HTTP “error”, we will do the same for the other implementation, the calling code will then get the returned code and will handle it (useful for authentication for example).

Since this is Java only and Android only, we need to test it in an Android test, so I wrote a little (these are Unit tests that run on the device).

@RunWith(AndroidJUnit4.class)
public class HttpInstrumentedTest {
@Test
public void getUser() throws InterruptedException {
new JavaHttpClient(
ApplicationProvider.())
.get("https://api.github.com/users/aosp", new HttpCallback() {
@Override
public void onFailure(String reason) {
();
synchronized (HttpInstrumentedTest.this) {
HttpInstrumentedTest.this.notify();
}
}

@Override
public void onResponse(String body, int code) {
(code, HttpURLConnection.);
synchronized (HttpInstrumentedTest.this) {
HttpInstrumentedTest.this.notify();
}
}
});

synchronized (this) {
this.wait();
}
}

(I know is better, it just does not run on my iPad)

Apple (macOS and iOS)

Now this one is interesting. For two reasons: it is in C++, so we can stay in our CMake / Gtest context, and it runs on the computer (provided the computer has a fruit sign at the back).

I use the preprocessor directive to compile it out #ifdef __APPLE__ , and also in CMake if (APPLE) to add the needed libraries to the linking: “Foundation” namely. The source file needs to have the .mm extension: the Apple NS* stuff is Objective-C. (Any computer archaeologists that know what stands for?)

void AppleHttpClient::get(const std::string &url,
const std::shared_ptr<xptuto::HttpCallback> &callback) {

auto session = [NSURLSession sharedSession];
auto request =
[NSURLRequest requestWithURL:
[[NSURL alloc] initWithString:(String::fromCpp(url))]];

__block auto cb = callback;

NSURLSessionDataTask *task =
[session dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
std::string_view body;

if (data && data.length) {
body = {(char *) [data bytes], data.length};
}
...
auto httpResponse = (NSHTTPURLResponse *) response;
cb->on_response(body,
static_cast<int32_t>(httpResponse.statusCode));
...
}];

[task resume];
}

We cannot get more concise. This is much better than the Android implementation for a couple of reasons. First: there is no need to configure the cache, this is done automatically by iOS, no so much by MacOS where I did not see it work. But Mac OS is only for testing anyway now.

Second big thing, we use C++17 string_view. Which means we do not copy the data, we use it as it comes from the completion handler, and we pass by reference. Something to remember if you need to pass that string around.

POST and the other methods are about the same to implement with NSURLRequest.

Here is our Gtest:

TEST_F(Xptuto, AppleGetUserTest) {
auto appleHttp = std::make_shared<AppleHttpClient>();

auto p = promise;

appleHttp->get("https://api.github.com/users/aosp",
std::make_shared<HttpCallbackImpl>(
[p](const std::string_view & body, int32_t code) {
EXPECT_EQ(code, 200);
p->set_value();
}, [p](const std::string &) {
FAIL();
}));

if (future.wait_for(10s) != std::future_status::ready) {
FAIL();
}
}

The test knows nothing about the NSUrlRequest and is in the same .cpp file as our other tests. You do not need XCode to run it, it works in QtCreator, CLion or in the command line, you just need to be on MacOS.

Web

Now who thinks we need to implement this in Javascript?

Emscripten has C implementations of many browser functions, they come handy in many ways, it allows you to stay in the C++ context while leveraging the power of your platform, browsers are bound to have top notch HTTP implementations, aren’t they?

So here is the code, using the Fetch API of Emscripten also compiled out with ifdef preprocessor directives and CMake ifs.

void WebHttpClient::get(const std::string &url, 
const std::shared_ptr<HttpCallback> &callback) {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = downloadSucceeded;
attr.onerror = downloadFailed;
auto fetch = emscripten_fetch(&attr, url.c_str());
callbacks.insert({fetch->id, callback});
}
void downloadSucceeded(emscripten_fetch_t *fetch) {
if (WebHttpClient::callbacks.find(fetch->id)
!= WebHttpClient::callbacks.end()) {
auto callback = WebHttpClient::callbacks[fetch->id];

std::string_view body;
if (fetch->data && fetch->numBytes > 0) {
body = {fetch->data,
static_cast<std::string_view::size_type>(fetch->numBytes)};
}

callback->on_response(body, fetch->status);
WebHttpClient::callbacks.erase(fetch->id);
}
emscripten_fetch_close(fetch);
}

void downloadFailed(emscripten_fetch_t *fetch) {
if (WebHttpClient::callbacks.find(fetch->id)
!= WebHttpClient::callbacks.end()) {
auto callback = WebHttpClient::callbacks[fetch->id];
if (fetch->status) {
std::string_view body;
if (fetch->data && fetch->numBytes > 0) {
body = {fetch->data, static_cast<std::string_view::size_type>(fetch->numBytes)};
}

callback->on_response(body, fetch->status);
} else {
callback->on_failure("Download failed");
}
WebHttpClient::callbacks.erase(fetch->id);
}
emscripten_fetch_close(fetch);
}

Because this is C, we cannot use std::function or smart pointers, so I keep track of the callbacks in a static unordered_map (in the .hpp file), this way I can reconnect a callback to a “fetch” object.

Again, C++17 string_view. Just like in the Apple implementation. POST and other methods are similar. One thing to notice is that this API considers a failure anything that is HTTP 400 and above, we don’t, failure will only be when there is no status. Hence the check and the on_response() call in the downloadFailed() callback.

Last thing to know: the callbacks are called in the calling thread, so if you are blocking the calling thread waiting for the calback, you are in a deadlock.

We can see it clearly in our test:

TEST_F(Xptuto, WebGetUserTest) {
auto webHttp = std::make_shared<WebHttpClient>();

auto p = promise;

webHttp->get("https://api.github.com/users/aosp", std::make_shared<HttpCallbackImpl>(
[p](const std::string_view &body, int32_t code) {
EXPECT_EQ(code, 200);
p->set_value();
}, [p](const std::string &) {
FAIL();
}));

if (future.wait_for(10s) != std::future_status::ready) {
FAIL();
}
}

This fails, it waits 10 seconds then fails and our callback gets executed. I still wrote a test so I could exercise the code and see if the calls are made and if caching works fine (it does, you bet… this is in production).

In general tests with threads that block the main thread fail in Web Assembly, I have not found a solution for that yet.

I any case we are good to go now, we can do HTTP on our 3 platforms, JSON was done in the last story, let’s finish up our client.

Finishing up our Client

Get a User from GitHub

Easy enough:

void XptutoImpl::get_user(const std::string &login, const std::shared_ptr<GetUserCb> &cb) {
client->get("https://api.github.com/users/" + login,
std::make_shared<HttpCallbackImpl>(
[cb](const std::string_view &body, int32_t code) {
if (!std::empty(body)) {
User user = nlohmann::json::parse(body);
cb->on_success(user);
} else {
cb->on_error("error");}
},
[cb](const std::string &) {
cb->on_error("error");
}));
}

Get All the Repositories for a User

void XptutoImpl::get_repos_for_user(const User &user, const std::shared_ptr<GetReposCb> &cb) {
client->get("https://api.github.com/users/"
+ user.login + "/repos",
std::make_shared<HttpCallbackImpl>(
[cb, user](const std::string_view &body, int32_t code) {
if (!std::empty(body)) {
std::vector<Repo> repos = nlohmann::json::parse(body);
cb->on_success(repos, user);
} else {
cb->on_error("error"); }
},
[cb](const std::string &) {
cb->on_error("error");
}));
}

Similar, nothing much, nlohmann::json knows about our models and can magically make lists of them (or sets or whatever STL).

Use in View Code

In Java we can use Anonymous classes to implement the callbacks, in C++ we have implemented functional callbacks, in Javascript it is a bit more tricky, extending classes with Embind is not super simple, but it is after all only configuration.

Here is our base class:

class JSInterface {

public:
explicit JSInterface(emscripten::val callbacks) : myCallbacks(callbacks) {}

emscripten::val myCallbacks;
};

And thanks to C++ multiple inheritance we can use it plus our Callback classes as the bases:

class JSGetReposCb : public xptuto::GetReposCb, JSInterface {
public:
explicit JSGetReposCb(emscripten::val callbacks) : JSInterface(std::move(callbacks)) {}

void on_success(const std::vector<Repo> &repos, const User &user) override {
emscripten::val on_successCB = myCallbacks["on_success"];
on_successCB(repos, user);
}

void on_error(const std::string &error) override {
emscripten::val on_errorCB = myCallbacks["on_error"];
on_errorCB(error);
}
};

Then it takes a bit of emscripten calls to register them:

emscripten::class_<GetReposCb>("GetReposCb")
.smart_ptr<std::shared_ptr<GetReposCb>>("GetReposCb");

emscripten::class_<JSGetReposCb, emscripten::base<GetReposCb>>("JSGetReposCb")
.smart_ptr_constructor("JSGetReposCb", &std::make_shared<JSGetReposCb, emscripten::val>);

Boy, this is ugly but we have to go through that.

And in our View, we can use something similar to our C++ functional implementation:

x.get_repos_for_user(user, new .JSGetReposCb({
on_error: function(error) {
.error(error)
},
on_success: function(repos, user) {
for (var i = 0; i < repos.size(); i++) {
.log('Got repo with name: ' + repos.get(i).name);
}
}
}));

I find this pretty clear actually.

Now this is just an implementation of ReST but imagine there could be a whole lot of stuff done between getting the data from a server and passing it to the view. (There is in the case of the projects I tend to work on).

Now why do I do nothing with the view just yet? Because I would like to go through threading first. Threading will be a necessary evil because the View needs to be accessed from a specific thread, always. And the work needs to be done from a background thread. This specific thread is bound to each platform.

Also read:

Introduction:

We present our technical choice.

1. Project Setup

We will configure a project that compiles and runs on iOS/Xcode, Android Studio and Emscripten/CMake and show how to run and debug the 3 of them.

2. Pass objects around

In this one we will show how to pass objects around from the business logic layer to the various view layers.

3. Unit test

Here we setup unit tests in the C++ code using the commonly used Google Test framework.

5. Multi-Threading

Yes, you can use threads in all three environments in a portable way!

6. Offline data with SQLite

Adding SQLite database is simple as ever.

The Startup

Medium's largest active publication, followed by +706K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store