blob: b5d3256c3c7104e0985741cb2d165794acef5677 [file] [log] [blame]
Laurent Carlier749294b2020-06-01 09:03:17 +01001//
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +00002// Copyright © 2017 Arm Ltd. All rights reserved.
3// SPDX-License-Identifier: MIT
4//
Matthew Benthamf48afc62020-01-15 17:55:08 +00005#include <armnn/Logging.hpp>
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +00006
Sadik Armagan232cfc22019-03-13 18:33:10 +00007#if defined(ARMNN_CAFFE_PARSER)
8#include <armnnCaffeParser/ICaffeParser.hpp>
9#endif
10#if defined(ARMNN_ONNX_PARSER)
11#include <armnnOnnxParser/IOnnxParser.hpp>
12#endif
13#if defined(ARMNN_SERIALIZER)
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000014#include <armnnSerializer/ISerializer.hpp>
Sadik Armagan232cfc22019-03-13 18:33:10 +000015#endif
16#if defined(ARMNN_TF_PARSER)
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000017#include <armnnTfParser/ITfParser.hpp>
Sadik Armagan232cfc22019-03-13 18:33:10 +000018#endif
19#if defined(ARMNN_TF_LITE_PARSER)
20#include <armnnTfLiteParser/ITfLiteParser.hpp>
21#endif
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000022
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000023#include <HeapProfiling.hpp>
Matthew Sloyan0663d662020-09-14 11:47:26 +010024#include <armnn/utility/NumericCast.hpp>
Colm Donelanb524ca02020-10-06 15:15:33 +010025#include <armnn/utility/StringUtils.hpp>
26
27/*
28 * Historically we use the ',' character to separate dimensions in a tensor shape. However, cxxopts will read this
29 * an an array of values which is fine until we have multiple tensors specified. This lumps the values of all shapes
30 * together in a single array and we cannot break it up again. We'll change the vector delimiter to a '.'. We do this
31 * as close as possible to the usage of cxxopts to avoid polluting other possible uses.
32 */
33#define CXXOPTS_VECTOR_DELIMITER '.'
34#include <cxxopts/cxxopts.hpp>
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000035
Colm Donelan5b5c2222020-09-09 12:48:16 +010036#include <fmt/format.h>
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000037
Les Bell10e6be42019-03-28 12:26:46 +000038#include <cstdlib>
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000039#include <fstream>
Les Bell10e6be42019-03-28 12:26:46 +000040#include <iostream>
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000041
42namespace
43{
44
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000045armnn::TensorShape ParseTensorShape(std::istream& stream)
46{
47 std::vector<unsigned int> result;
48 std::string line;
49
50 while (std::getline(stream, line))
51 {
David Monahana8837bf2020-04-16 10:01:56 +010052 std::vector<std::string> tokens = armnn::stringUtils::StringTokenizer(line, ",");
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000053 for (const std::string& token : tokens)
54 {
55 if (!token.empty())
56 {
57 try
58 {
Matthew Sloyan0663d662020-09-14 11:47:26 +010059 result.push_back(armnn::numeric_cast<unsigned int>(std::stoi((token))));
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000060 }
61 catch (const std::exception&)
62 {
Derek Lamberti08446972019-11-26 16:38:31 +000063 ARMNN_LOG(error) << "'" << token << "' is not a valid number. It has been ignored.";
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000064 }
65 }
66 }
67 }
68
Matthew Sloyan0663d662020-09-14 11:47:26 +010069 return armnn::TensorShape(armnn::numeric_cast<unsigned int>(result.size()), result.data());
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000070}
71
Colm Donelanb524ca02020-10-06 15:15:33 +010072int ParseCommandLineArgs(int argc, char* argv[],
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +000073 std::string& modelFormat,
74 std::string& modelPath,
75 std::vector<std::string>& inputNames,
76 std::vector<std::string>& inputTensorShapeStrs,
77 std::vector<std::string>& outputNames,
78 std::string& outputPath, bool& isModelBinary)
79{
Colm Donelanb524ca02020-10-06 15:15:33 +010080 cxxopts::Options options("ArmNNConverter", "Convert a neural network model from provided file to ArmNN format.");
81 try
82 {
83 std::string modelFormatDescription("Format of the model file");
Sadik Armagan232cfc22019-03-13 18:33:10 +000084#if defined(ARMNN_CAFFE_PARSER)
Colm Donelanb524ca02020-10-06 15:15:33 +010085 modelFormatDescription += ", caffe-binary, caffe-text";
Sadik Armagan232cfc22019-03-13 18:33:10 +000086#endif
87#if defined(ARMNN_ONNX_PARSER)
Colm Donelanb524ca02020-10-06 15:15:33 +010088 modelFormatDescription += ", onnx-binary, onnx-text";
Sadik Armagan232cfc22019-03-13 18:33:10 +000089#endif
Les Bell10e6be42019-03-28 12:26:46 +000090#if defined(ARMNN_TF_PARSER)
Colm Donelanb524ca02020-10-06 15:15:33 +010091 modelFormatDescription += ", tensorflow-binary, tensorflow-text";
Sadik Armagan232cfc22019-03-13 18:33:10 +000092#endif
93#if defined(ARMNN_TF_LITE_PARSER)
Colm Donelanb524ca02020-10-06 15:15:33 +010094 modelFormatDescription += ", tflite-binary";
Sadik Armagan232cfc22019-03-13 18:33:10 +000095#endif
Colm Donelanb524ca02020-10-06 15:15:33 +010096 modelFormatDescription += ".";
97 options.add_options()
98 ("help", "Display usage information")
99 ("f,model-format", modelFormatDescription, cxxopts::value<std::string>(modelFormat))
100 ("m,model-path", "Path to model file.", cxxopts::value<std::string>(modelPath))
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000101
Colm Donelanb524ca02020-10-06 15:15:33 +0100102 ("i,input-name", "Identifier of the input tensors in the network. "
103 "Each input must be specified separately.",
104 cxxopts::value<std::vector<std::string>>(inputNames))
105 ("s,input-tensor-shape",
106 "The shape of the input tensor in the network as a flat array of integers, "
107 "separated by comma. Each input shape must be specified separately after the input name. "
108 "This parameter is optional, depending on the network.",
109 cxxopts::value<std::vector<std::string>>(inputTensorShapeStrs))
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000110
Colm Donelanb524ca02020-10-06 15:15:33 +0100111 ("o,output-name", "Identifier of the output tensor in the network.",
112 cxxopts::value<std::vector<std::string>>(outputNames))
113 ("p,output-path",
114 "Path to serialize the network to.", cxxopts::value<std::string>(outputPath));
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000115 }
Colm Donelanb524ca02020-10-06 15:15:33 +0100116 catch (const std::exception& e)
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000117 {
Colm Donelanb524ca02020-10-06 15:15:33 +0100118 std::cerr << e.what() << std::endl << options.help() << std::endl;
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000119 return EXIT_FAILURE;
120 }
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000121 try
122 {
Colm Donelanb524ca02020-10-06 15:15:33 +0100123 cxxopts::ParseResult result = options.parse(argc, argv);
124 if (result.count("help"))
125 {
126 std::cerr << options.help() << std::endl;
127 return EXIT_SUCCESS;
128 }
129 // Check for mandatory single options.
130 std::string mandatorySingleParameters[] = { "model-format", "model-path", "output-name", "output-path" };
131 bool somethingsMissing = false;
132 for (auto param : mandatorySingleParameters)
133 {
134 if (result.count(param) != 1)
135 {
136 std::cerr << "Parameter \'--" << param << "\' is required but missing." << std::endl;
137 somethingsMissing = true;
138 }
139 }
140 // Check at least one "input-name" option.
141 if (result.count("input-name") == 0)
142 {
143 std::cerr << "Parameter \'--" << "input-name" << "\' must be specified at least once." << std::endl;
144 somethingsMissing = true;
145 }
146 // If input-tensor-shape is specified then there must be a 1:1 match with input-name.
147 if (result.count("input-tensor-shape") > 0)
148 {
149 if (result.count("input-tensor-shape") != result.count("input-name"))
150 {
151 std::cerr << "When specifying \'input-tensor-shape\' a matching number of \'input-name\' parameters "
152 "must be specified." << std::endl;
153 somethingsMissing = true;
154 }
155 }
156
157 if (somethingsMissing)
158 {
159 std::cerr << options.help() << std::endl;
160 return EXIT_FAILURE;
161 }
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000162 }
Colm Donelanb524ca02020-10-06 15:15:33 +0100163 catch (const cxxopts::OptionException& e)
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000164 {
165 std::cerr << e.what() << std::endl << std::endl;
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000166 return EXIT_FAILURE;
167 }
168
169 if (modelFormat.find("bin") != std::string::npos)
170 {
171 isModelBinary = true;
172 }
Sadik Armagan232cfc22019-03-13 18:33:10 +0000173 else if (modelFormat.find("text") != std::string::npos)
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000174 {
175 isModelBinary = false;
176 }
177 else
178 {
Derek Lamberti08446972019-11-26 16:38:31 +0000179 ARMNN_LOG(fatal) << "Unknown model format: '" << modelFormat << "'. Please include 'binary' or 'text'";
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000180 return EXIT_FAILURE;
181 }
182
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000183 return EXIT_SUCCESS;
184}
185
Sadik Armagan232cfc22019-03-13 18:33:10 +0000186template<typename T>
187struct ParserType
188{
189 typedef T parserType;
190};
191
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000192class ArmnnConverter
193{
194public:
195 ArmnnConverter(const std::string& modelPath,
196 const std::vector<std::string>& inputNames,
197 const std::vector<armnn::TensorShape>& inputShapes,
198 const std::vector<std::string>& outputNames,
199 const std::string& outputPath,
200 bool isModelBinary)
201 : m_NetworkPtr(armnn::INetworkPtr(nullptr, [](armnn::INetwork *){})),
202 m_ModelPath(modelPath),
203 m_InputNames(inputNames),
204 m_InputShapes(inputShapes),
205 m_OutputNames(outputNames),
206 m_OutputPath(outputPath),
207 m_IsModelBinary(isModelBinary) {}
208
209 bool Serialize()
210 {
211 if (m_NetworkPtr.get() == nullptr)
212 {
213 return false;
214 }
215
216 auto serializer(armnnSerializer::ISerializer::Create());
217
218 serializer->Serialize(*m_NetworkPtr);
219
220 std::ofstream file(m_OutputPath, std::ios::out | std::ios::binary);
221
222 bool retVal = serializer->SaveSerializedToStream(file);
223
224 return retVal;
225 }
226
227 template <typename IParser>
228 bool CreateNetwork ()
229 {
Sadik Armagan232cfc22019-03-13 18:33:10 +0000230 return CreateNetwork (ParserType<IParser>());
231 }
232
233private:
234 armnn::INetworkPtr m_NetworkPtr;
235 std::string m_ModelPath;
236 std::vector<std::string> m_InputNames;
237 std::vector<armnn::TensorShape> m_InputShapes;
238 std::vector<std::string> m_OutputNames;
239 std::string m_OutputPath;
240 bool m_IsModelBinary;
241
242 template <typename IParser>
243 bool CreateNetwork (ParserType<IParser>)
244 {
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000245 // Create a network from a file on disk
246 auto parser(IParser::Create());
247
248 std::map<std::string, armnn::TensorShape> inputShapes;
249 if (!m_InputShapes.empty())
250 {
251 const size_t numInputShapes = m_InputShapes.size();
252 const size_t numInputBindings = m_InputNames.size();
253 if (numInputShapes < numInputBindings)
254 {
Colm Donelan5b5c2222020-09-09 12:48:16 +0100255 throw armnn::Exception(fmt::format(
256 "Not every input has its tensor shape specified: expected={0}, got={1}",
257 numInputBindings, numInputShapes));
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000258 }
259
260 for (size_t i = 0; i < numInputShapes; i++)
261 {
262 inputShapes[m_InputNames[i]] = m_InputShapes[i];
263 }
264 }
265
266 {
267 ARMNN_SCOPED_HEAP_PROFILING("Parsing");
268 m_NetworkPtr = (m_IsModelBinary ?
269 parser->CreateNetworkFromBinaryFile(m_ModelPath.c_str(), inputShapes, m_OutputNames) :
270 parser->CreateNetworkFromTextFile(m_ModelPath.c_str(), inputShapes, m_OutputNames));
271 }
272
273 return m_NetworkPtr.get() != nullptr;
274 }
275
Sadik Armagan232cfc22019-03-13 18:33:10 +0000276#if defined(ARMNN_TF_LITE_PARSER)
277 bool CreateNetwork (ParserType<armnnTfLiteParser::ITfLiteParser>)
278 {
279 // Create a network from a file on disk
280 auto parser(armnnTfLiteParser::ITfLiteParser::Create());
281
282 if (!m_InputShapes.empty())
283 {
284 const size_t numInputShapes = m_InputShapes.size();
285 const size_t numInputBindings = m_InputNames.size();
286 if (numInputShapes < numInputBindings)
287 {
Colm Donelan5b5c2222020-09-09 12:48:16 +0100288 throw armnn::Exception(fmt::format(
289 "Not every input has its tensor shape specified: expected={0}, got={1}",
290 numInputBindings, numInputShapes));
Sadik Armagan232cfc22019-03-13 18:33:10 +0000291 }
292 }
293
294 {
295 ARMNN_SCOPED_HEAP_PROFILING("Parsing");
296 m_NetworkPtr = parser->CreateNetworkFromBinaryFile(m_ModelPath.c_str());
297 }
298
299 return m_NetworkPtr.get() != nullptr;
300 }
301#endif
302
303#if defined(ARMNN_ONNX_PARSER)
304 bool CreateNetwork (ParserType<armnnOnnxParser::IOnnxParser>)
305 {
306 // Create a network from a file on disk
307 auto parser(armnnOnnxParser::IOnnxParser::Create());
308
309 if (!m_InputShapes.empty())
310 {
311 const size_t numInputShapes = m_InputShapes.size();
312 const size_t numInputBindings = m_InputNames.size();
313 if (numInputShapes < numInputBindings)
314 {
Colm Donelan5b5c2222020-09-09 12:48:16 +0100315 throw armnn::Exception(fmt::format(
316 "Not every input has its tensor shape specified: expected={0}, got={1}",
317 numInputBindings, numInputShapes));
Sadik Armagan232cfc22019-03-13 18:33:10 +0000318 }
319 }
320
321 {
322 ARMNN_SCOPED_HEAP_PROFILING("Parsing");
323 m_NetworkPtr = (m_IsModelBinary ?
324 parser->CreateNetworkFromBinaryFile(m_ModelPath.c_str()) :
325 parser->CreateNetworkFromTextFile(m_ModelPath.c_str()));
326 }
327
328 return m_NetworkPtr.get() != nullptr;
329 }
330#endif
331
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000332};
333
334} // anonymous namespace
335
Colm Donelanb524ca02020-10-06 15:15:33 +0100336int main(int argc, char* argv[])
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000337{
338
Sadik Armagan232cfc22019-03-13 18:33:10 +0000339#if (!defined(ARMNN_CAFFE_PARSER) \
340 && !defined(ARMNN_ONNX_PARSER) \
341 && !defined(ARMNN_TF_PARSER) \
342 && !defined(ARMNN_TF_LITE_PARSER))
Derek Lamberti08446972019-11-26 16:38:31 +0000343 ARMNN_LOG(fatal) << "Not built with any of the supported parsers, Caffe, Onnx, Tensorflow, or TfLite.";
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000344 return EXIT_FAILURE;
345#endif
346
347#if !defined(ARMNN_SERIALIZER)
Derek Lamberti08446972019-11-26 16:38:31 +0000348 ARMNN_LOG(fatal) << "Not built with Serializer support.";
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000349 return EXIT_FAILURE;
350#endif
351
352#ifdef NDEBUG
353 armnn::LogSeverity level = armnn::LogSeverity::Info;
354#else
355 armnn::LogSeverity level = armnn::LogSeverity::Debug;
356#endif
357
358 armnn::ConfigureLogging(true, true, level);
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000359
360 std::string modelFormat;
361 std::string modelPath;
362
363 std::vector<std::string> inputNames;
364 std::vector<std::string> inputTensorShapeStrs;
365 std::vector<armnn::TensorShape> inputTensorShapes;
366
367 std::vector<std::string> outputNames;
368 std::string outputPath;
369
370 bool isModelBinary = true;
371
372 if (ParseCommandLineArgs(
373 argc, argv, modelFormat, modelPath, inputNames, inputTensorShapeStrs, outputNames, outputPath, isModelBinary)
374 != EXIT_SUCCESS)
375 {
376 return EXIT_FAILURE;
377 }
378
379 for (const std::string& shapeStr : inputTensorShapeStrs)
380 {
381 if (!shapeStr.empty())
382 {
383 std::stringstream ss(shapeStr);
384
385 try
386 {
387 armnn::TensorShape shape = ParseTensorShape(ss);
388 inputTensorShapes.push_back(shape);
389 }
390 catch (const armnn::InvalidArgumentException& e)
391 {
Derek Lamberti08446972019-11-26 16:38:31 +0000392 ARMNN_LOG(fatal) << "Cannot create tensor shape: " << e.what();
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000393 return EXIT_FAILURE;
394 }
395 }
396 }
397
398 ArmnnConverter converter(modelPath, inputNames, inputTensorShapes, outputNames, outputPath, isModelBinary);
399
Derek Lambertic9e52792020-03-11 11:42:26 +0000400 try
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000401 {
Derek Lambertic9e52792020-03-11 11:42:26 +0000402 if (modelFormat.find("caffe") != std::string::npos)
403 {
Sadik Armagan232cfc22019-03-13 18:33:10 +0000404#if defined(ARMNN_CAFFE_PARSER)
Derek Lambertic9e52792020-03-11 11:42:26 +0000405 if (!converter.CreateNetwork<armnnCaffeParser::ICaffeParser>())
406 {
407 ARMNN_LOG(fatal) << "Failed to load model from file";
408 return EXIT_FAILURE;
409 }
Sadik Armagan232cfc22019-03-13 18:33:10 +0000410#else
Derek Lambertic9e52792020-03-11 11:42:26 +0000411 ARMNN_LOG(fatal) << "Not built with Caffe parser support.";
412 return EXIT_FAILURE;
Sadik Armagan232cfc22019-03-13 18:33:10 +0000413#endif
Derek Lambertic9e52792020-03-11 11:42:26 +0000414 }
415 else if (modelFormat.find("onnx") != std::string::npos)
416 {
Sadik Armagan232cfc22019-03-13 18:33:10 +0000417#if defined(ARMNN_ONNX_PARSER)
Derek Lambertic9e52792020-03-11 11:42:26 +0000418 if (!converter.CreateNetwork<armnnOnnxParser::IOnnxParser>())
419 {
420 ARMNN_LOG(fatal) << "Failed to load model from file";
421 return EXIT_FAILURE;
422 }
Sadik Armagan232cfc22019-03-13 18:33:10 +0000423#else
Derek Lambertic9e52792020-03-11 11:42:26 +0000424 ARMNN_LOG(fatal) << "Not built with Onnx parser support.";
425 return EXIT_FAILURE;
Sadik Armagan232cfc22019-03-13 18:33:10 +0000426#endif
Derek Lambertic9e52792020-03-11 11:42:26 +0000427 }
428 else if (modelFormat.find("tensorflow") != std::string::npos)
429 {
Sadik Armagan232cfc22019-03-13 18:33:10 +0000430#if defined(ARMNN_TF_PARSER)
Derek Lambertic9e52792020-03-11 11:42:26 +0000431 if (!converter.CreateNetwork<armnnTfParser::ITfParser>())
432 {
433 ARMNN_LOG(fatal) << "Failed to load model from file";
434 return EXIT_FAILURE;
435 }
Sadik Armagan232cfc22019-03-13 18:33:10 +0000436#else
Derek Lambertic9e52792020-03-11 11:42:26 +0000437 ARMNN_LOG(fatal) << "Not built with Tensorflow parser support.";
438 return EXIT_FAILURE;
Sadik Armagan232cfc22019-03-13 18:33:10 +0000439#endif
Derek Lambertic9e52792020-03-11 11:42:26 +0000440 }
441 else if (modelFormat.find("tflite") != std::string::npos)
442 {
Sadik Armagan232cfc22019-03-13 18:33:10 +0000443#if defined(ARMNN_TF_LITE_PARSER)
Derek Lambertic9e52792020-03-11 11:42:26 +0000444 if (!isModelBinary)
445 {
446 ARMNN_LOG(fatal) << "Unknown model format: '" << modelFormat << "'. Only 'binary' format supported \
447 for tflite files";
448 return EXIT_FAILURE;
449 }
Sadik Armagan232cfc22019-03-13 18:33:10 +0000450
Derek Lambertic9e52792020-03-11 11:42:26 +0000451 if (!converter.CreateNetwork<armnnTfLiteParser::ITfLiteParser>())
452 {
453 ARMNN_LOG(fatal) << "Failed to load model from file";
454 return EXIT_FAILURE;
455 }
456#else
457 ARMNN_LOG(fatal) << "Not built with TfLite parser support.";
458 return EXIT_FAILURE;
459#endif
460 }
461 else
Sadik Armagan232cfc22019-03-13 18:33:10 +0000462 {
Derek Lambertic9e52792020-03-11 11:42:26 +0000463 ARMNN_LOG(fatal) << "Unknown model format: '" << modelFormat << "'";
Sadik Armagan232cfc22019-03-13 18:33:10 +0000464 return EXIT_FAILURE;
465 }
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000466 }
Derek Lambertic9e52792020-03-11 11:42:26 +0000467 catch(armnn::Exception& e)
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000468 {
Derek Lambertic9e52792020-03-11 11:42:26 +0000469 ARMNN_LOG(fatal) << "Failed to load model from file: " << e.what();
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000470 return EXIT_FAILURE;
471 }
472
473 if (!converter.Serialize())
474 {
Derek Lamberti08446972019-11-26 16:38:31 +0000475 ARMNN_LOG(fatal) << "Failed to serialize model";
Nattapat Chaimanowong4fbae332019-02-14 15:28:02 +0000476 return EXIT_FAILURE;
477 }
478
479 return EXIT_SUCCESS;
480}