|
Post by jontara on Nov 27, 2017 22:25:13 GMT
Sometimes you might have some portable C or C++ code that you'd like to wrap in a Rhodes extension - but with no need to call any "native" APIs or frameworks in e.g. Objective-c/iOS Java/Android, etc.
When implementing an extension in "native" language, passed data structures (strings, hashs, arrays, etc.) are converted from Ruby/C data structures to "native" data structures (Objective-C, Java, etc.) that can be manipulated with native APIs. And then again, when your extension code passes back native data structures, they are again converted back to Ruby/C data structures.
If not using any native APIs, this extra conversion is unnecessary. And then you still have the problem of calling the C/C++ code for platforms where c/c++ (or language that can easily call C/C++ - like Objective-C) is not the "native" language.
I would like to create an extension that calls some portable C code and returns results to the caller. I use some simple data types - float, int, boolean and some basic data structures - strings and hashs. I would like to leverage the ability to use an XML file to define the API and create constants for my hash keys, etc. (so, I prefer not to use the older native-extension mechanism from 2.x).
What is the best way to approach this?
(This is a replay of a question asked to Tau support. I've been asked to re-post it here, as the solution is potentially of use to a broader audience.)
|
|
|
Post by jontara on Nov 29, 2017 2:13:44 GMT
I'll go ahead and repost the answer I got from support, for the benefit of others. I have a follow-on question I'll post after.
Note: there was a test.zip attached on Zendesk with a complete example app. I was unable to attach it here, because it exceeds the attachment size limit. Maybe Tau can attach it or make it available some other way. ---- This is example of pure C++ extension. Prepared and tested for iOS and Android Prepared from generated extension. This is steps :
rhodes app Test cd Test rhodes extension Example
add “example” to extension’s list in build.yml
in /app/index.erb set link to ExampleTest page
in /extensions/Example/ext/shared/generated/cpp/ located C++ wrappers for CommonAPI
in /extensions/Example/ext/shared/generated/stub/ example stub C++ implementation copy /extensions/Example/ext/shared/generated/stub/example_stub_impl.cpp into /extensions/Example/ext/shared/ and rename it to example_impl.cpp
So now we should change iOS Xcode project: remove all generated and implementation files and replace it to:
/extensions/Example/ext/shared/generated/cpp/Example_js_wrap.cpp /extensions/Example/ext/shared/generated/cpp/Example_ruby_wrap.cpp /extensions/Example/ext/shared/generated/cpp/ExampleBase.cpp /extensions/Example/ext/shared/generated/example_api_init.cpp /extensions/Example/ext/shared/generated/example_js_api.cpp /extensions/Example/ext/shared/generated/example_ruby_api.c /extensions/Example/ext/shared/example_impl.cpp
The same for Android: remove /extensions/Example/ext/platform/android/ext_java.files remove also source java list from /extensions/Example/ext.yml
set /extensions/Example/ext/platform/android/ext_native.files to :
ext/shared/generated/cpp/Example_js_wrap.cpp ext/shared/generated/cpp/Example_ruby_wrap.cpp ext/shared/generated/cpp/ExampleBase.cpp ext/shared/generated/example_api_init.cpp ext/shared/generated/example_js_api.cpp ext/shared/generated/example_ruby_api.c ext/shared/example_impl.cpp
finalize implementation in ext/shared/example_impl.cpp remove get/set Property (will used from base implementation class) implement getInitialDefaultID() in CExampleSingletonImpl class implement enumerate() in CExampleSingletonImpl class write code in method
|
|
|
Post by jontara on Nov 29, 2017 2:49:21 GMT
I have managed to blunder through getting data passed to my C function. But I am struggling with how to return results. The example is very simple, with just a couple of functions that return strings or integers. I'd appreciate any pointers on how to return a hash - and, in particular, a hash containing some strings and another hash at second level. I managed to do this in the past in Objective-C for iOS but not easily finding clear examples in the Rhodes source code for doing it in C++.
Some observations that might help others:
- Total parameters to a method are limited to 8, including results. So, you cannot have more than 7 parameters. (It a limitation of a C++ macro, I suppose it could be easily expanded in Rhodes source.)
- Apparently, when you pass in a hash, the values are always strings. Rhodes converts various types to string. So, you have to use e.g. atoi() atof() etc. to get integers or floats. Unclear to me what happens if you pass a nested hash. Fortunately, I do not need to do this - at least as an input parameter. However - I do need to do so with the return value.
For clarity, I have a single function, defined in my <extension>_impl.cpp as:
virtual void gradeSketchFh( const rho::Hashtable<rho::String, rho::String>& pr, const rho::Hashtable<rho::String, rho::String>& imIn, rho::apiGenerator::CMethodResult& oResult) {
pr values might be string, float, or boolean (and are declared as such in the XML). They all come across as string objects, though. So, for example, I can get a float value with e.g.: double allowPerTotal = atof(pr.get(rho::grading::HK_ALLOW_PER_TOTAL).c_str());
(Actually, you could use strtod and you wouldn't need c_str())
Boolean values are either the string "true", or an empty string.
bool retDil = pr.get(rho::grading::HK_RET_DIL) == "true" ? true : false;
No clue how hashs nested inside of hashes are retrieved, but I don't have that need.
---- For return a hash, I GUESS you need a Hashtable or rho::Hashtable (which? Or does it matter?) and use oResult.set() to set result to the hash table. I also came across some code that calls setProperty() with a value in first parameter, and hash table reference in the second.
An example app showing the use of more data types would be very useful!
|
|
|
Post by Dmitry Soldatenkov on Nov 29, 2017 11:35:27 GMT
|
|
|
Post by jontara on Nov 30, 2017 7:33:39 GMT
I tried to follow the example, but no luck.
I can pass back first-level strings successfully using either my original method of building a rho::Hash in "results" and oResult.set(results) or else build a rho::apiGenerator::StringifyHash and use oResult.set(resultJson);
But when I set the second-level hash into the main result hash, the second hash is not returned.
A workaround would be to change my XML so that a string is expected to be returned for the second-level hash, rather than a hash.
That would be awkward, though, because I see the helper produces a bastardized kind of JSON with only strings. (e.g. float values have quotation marks around them).
And in any case, I could have accomplished that without going to the trouble of returning JSON at the top level. (Just use a Hash and return a hash of only strings, one of the strings is opaque JSON.)
I'll include abbreviated code below (I omitted some keys). See the FIXME: line.
rho::apiGenerator::StringifyHash mh;
mh.set(HK_FH_MIS_ONE_LONG_SOLID_BLOB, metrics.fhMisOneLongSolidBlob); mh.set(HK_FH_LARGE_TOL, metrics.fhLargeTol); mh.set(HK_MAX_ADD_BLOB_LEN_NORM, metrics.maxAddBlobLenNorm); rho::String metricsJson; mh.toString(metricsJson);
RAWLOG_INFO1("metricsJson: '%s'", metricsJson.c_str());
RAWLOG_INFO("populating BWC strings from returned emxArrays...");
rho::apiGenerator::StringifyHash result; result.set(HK_ADD_PIX_BWC, emxDataToBwcstr(im.addPixBWC, nCols, nRows)); result.set(HK_COR_PIX_BWC, emxDataToBwcstr(im.corPixBWC, nCols, nRows)); result.set(HK_MIS_PIX_BWC, emxDataToBwcstr(im.misPixBWC, nCols, nRows)); result.set(HK_SKETCH_BWC_DIL, prC.retDil ? emxDataToBwcstr(im.sketchBWCDil, nCols, nRows) : ""); result.set(HK_SOL_BWC_DIL, prC.retDil ? emxDataToBwcstr(im.solBWCDil, nCols, nRows) : ""); result.set(HK_METRICS, mh); // FIXME: This line seemingly does nothing!
rho::String resultJson; result.toString(resultJson);
RAWLOG_INFO("...created return hash, setting methodResult"); oResult.setJSON(resultJson);
XML for return is like: (again, abbreviated)
<RETURN type="HASH"> <PARAMS>
<PARAM name="addPixBwc" type="STRING"/> <PARAM name="corPixBwc" type="STRING"/> <PARAM name="misPixBwc" type="STRING"/> <PARAM name="sketchBwcDil" type="STRING"/> <PARAM name="solBwcDil" type="STRING"/>
<PARAM name="metrics" type="HASH"> <PARAMS> <PARAM name="fhLargeTol" type="BOOLEAN"/> <PARAM name="fhMisOneLongSolidBlob" type="BOOLEAN"/> <PARAM name="maxAddBlobLenNorm" type="FLOAT"/> </PARAMS> </PARAM>
</PARAMS> </RETURN>
I think the easy work-around is simply add additional top-level keys for metrics values, and give the keys for metrics all a common prefix e.g. "metric_" and then I can easily reconstruct the hash in the caller.
What am I missing here? Is there some JSON declaration I have to make in XML?
Note: I couldn't log resultJson in the CPP code, because RAWLOG has a character limit and the strings _BWC are very long. But I log it in the caller after return, and "metrics" is missing.
|
|
|
Post by jontara on Nov 30, 2017 8:22:53 GMT
(This should probably be moved to Rhomobile Platform/general. Sorry, I had originally posted this to the wrong forum section)
|
|
|
Post by jontara on Nov 30, 2017 23:35:32 GMT
It turns out the issue was just that I was logging a very large object. The logging string length limitations are different between RAWLOG in C/C++ and RhoLog in Ruby. I hadn't looked at the logging in the caller (Ruby) closely enough to realize that only the first two of my few large strings were being logged. And their keys precede those of the second-level hash key.
But I went ahead and implemented the work-around for now.
I think there is another way besides the JSON helper. It is used in one place in Rhodes, in NetworkImpl.cpp. It involves using oResult.getStringHashL2(). Network is the only place that uses this function. It's not a general solution, but should work for my case.
|
|
|
Post by jontara on Dec 1, 2017 0:47:50 GMT
I guess for C++ extension, there is no support for conversion to correct type on return, int/float/bool? Only string (and hash and I guess array) supported?
I have only been able to set string results for each key. They don't seem to get converted to the types declared in the XML.
With my previous iOS/Objective-C implementation, the types were converted properly.
|
|
|
Post by jontara on Dec 1, 2017 1:22:22 GMT
Confirmed that I can just use .getStringHashL2() to stuff a second-level hash, rather than using the JSON helper.
Still face issue that non-string values come across in Ruby as string. But of course in my code below I set it as string. num_to_string is my own small template.
Sample code:
rho::Hashtable <rho::String,rho::String>& result = oResult.getStringHash();
RAWLOG_INFO("populating BWC strings from returned emxArrays..."); // Note emxDataToBwcstr returns rho::String result[HK_ADD_PIX_BWC] = emxDataToBwcstr(im.addPixBWC, nCols, nRows); result[HK_COR_PIX_BWC] = emxDataToBwcstr(im.corPixBWC, nCols, nRows); result[HK_MIS_PIX_BWC] = emxDataToBwcstr(im.misPixBWC, nCols, nRows); result[HK_SKETCH_BWC_DIL] = prC.retDil ? emxDataToBwcstr(im.sketchBWCDil, nCols, nRows) : ""; result[HK_SOL_BWC_DIL] = prC.retDil ? emxDataToBwcstr(im.solBWCDil, nCols, nRows) : "";
rho::Hashtable <rho::String,rho::String>& mh = oResult.getStringHashL2()["metrics"];
RAWLOG_INFO("populating metrics...");
mh.put(HK_FH_MIS_ONE_LONG_SOLID_BLOB, metrics.fhMisOneLongSolidBlob ? "1" : "0"); mh.put(HK_FH_LARGE_TOL, metrics.fhLargeTol ? "1" : "0"); mh.put(HK_MAX_ADD_BLOB_LEN_NORM, num_to_string(metrics.maxAddBlobLenNorm));
for (it_type iterator = mh.begin(); iterator != mh.end(); iterator++) { RAWLOG_INFO2("mh key:%s value: '%s'", iterator->first.c_str(), iterator->second.c_str()); }
RAWLOG_INFO("...created return hash, setting methodResult"); oResult.set(result);
|
|