GDExtension, bind method not found:
Asked Answered
M

2

0

I been following the official tutorial but can not call even a simple C++ function from GDScript. I can not figure out what the problem is.

I had this code working for Godot 3.5, what I am doing now is trying to convert is to 4.1.
The error i get when trying to call any function, in this example close() is this:
Invalid call. Nonexistent function 'close' in base 'TcpSocket'.

Here is the code for my class:

/*************************************************************************/
/*  TcpSocket.cpp                                                        */
/*************************************************************************/
/*                       This file is part of:                           */
/*                       GODOT TCPSOCK PLUGIN                            */
/*             https://github.com/flodihn/GodotTcp ocket                 */
/*************************************************************************/
/* Copyright (c) 2023 Christian Flodihn.                                 */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include <stdio.h>

#include "tcp_socket.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

using namespace std;
using namespace godot;

using namespace boost::asio;
using error_code = boost::system::error_code;

/*
 * TODO: Use shared or unique pointers for these variables.
 *
 * Normally it is bad practice to put variables like this
 * outside the class in a C++ project. But because of 
 * issues BOOST it seems these can only be put in a class
 * using shared or unique pointers.
 *
 * Implications of putting variables here is that MULTIPLE
 * INSTANCES of TcpSocket class WILL PROBABLY NOT WORK.
 * In practice this is not as problem as long as the code
 * is only used as a client.
 */
boost::asio::io_service ioService;
boost::asio::io_context ioContext;
boost::asio::ssl::context sslContext(boost::asio::ssl::context::tls);
boost::asio::ip::tcp::socket tcpSocket(ioService);
boost::asio::ssl::stream<ip::tcp::socket> secureSocket(ioContext, sslContext);

void TcpSocket::_bind_methods() {
	UtilityFunctions::print("Binding methods...");
	ClassDB::bind_method(D_METHOD("connect_to_host", "host", "port"), &TcpSocket::connect_to_host);
	ClassDB::bind_method(D_METHOD("close"), &TcpSocket::close);
	ClassDB::bind_method(D_METHOD("set_message_header_size"), &TcpSocket::set_message_header_size);
	ClassDB::bind_method(D_METHOD("set_message_buffer"), &TcpSocket::set_message_buffer);
	ClassDB::bind_method(D_METHOD("set_ssl"), &TcpSocket::set_ssl);
	ClassDB::bind_method(D_METHOD("set_blocking"), &TcpSocket::set_blocking);
	ClassDB::bind_method(D_METHOD("set_debug", "trueOrFalse"), &TcpSocket::set_debug);

	ClassDB::bind_method(D_METHOD("receive_message"), &TcpSocket::receive_message);
	ClassDB::bind_method(D_METHOD("send_message"), &TcpSocket::send_message);
	
	//ClassDB::bind_method(D_METHOD("ntohs"), &TcpSocket::ntohs);
	//ClassDB::bind_method(D_METHOD("htonl"), &TcpSocket::htonl);
	
	//ClassDB::bind_property<TcpSocket, bool>(D_METHOD("debug"), &TcpSocket::debug, false);
}

TcpSocket::TcpSocket() {
	receiveData.numBytesReceived = 0;
	// The state is ignored if we are in blocking mode.
	receiveData.state = NonBlockingState::RECEIVE_HEADER;
}

TcpSocket::~TcpSocket() {
	// Add your cleanup here.
}

void TcpSocket::_process(double delta) {
}

int TcpSocket::connect_to_host(Variant host, int port)
{
	debug_print("Connecting to host...");
	int error = _connect(host.stringify().utf8().get_data(), port);

	if(error) {
		debug_print("Failed to connect.");
	} else {
		debug_print("Connected successfully.");
	}

	return error;
}

void TcpSocket::close()
{
	::free(receiveData.header_buffer);
	::free(receiveData.packet_buffer);
	//socketWrapper.close();
}

void TcpSocket::set_ssl(bool trueOrFalse)
{
	//socketWrapper.sslEnabled = trueOrFalse;
}

void TcpSocket::set_blocking(bool trueOrFalse)
{
	blocking = trueOrFalse;
	//socketWrapper.blocking = trueOrFalse;
}

void TcpSocket::set_debug(bool trueOrFalse)
{
	debug = trueOrFalse;
}

void TcpSocket::set_message_header_size(int size)
{
	if (!(size == 2 || size == 4)) {
		debug_print("RawSocket.set_message_header_size error: Size must be 2 or 4 bytes.\n");
		return;
	}

	headerSize = size;
	receiveData.header_buffer = (char*) malloc(headerSize);
	receiveData.numBytesToReceive = headerSize;
}

void TcpSocket::set_message_buffer(godot::Ref<godot::StreamPeerBuffer> messageBufferRef)
{
	messageBuffer = messageBufferRef.ptr();
	receiveData.packet_buffer = (char*) malloc(messageBuffer->get_size());
}

void TcpSocket::debug_print(const char* output)
{
	if (!debug) return;
	UtilityFunctions::print(output);
}

int TcpSocket::receive_message()
{
	if(blocking) {
		return blocking_receive_message();
	}

	return non_blocking_receive_message();
}

int TcpSocket::blocking_receive_message()
{
	debug_print("blocking_receive_message: Waiting for header...\n");
	
	unsigned short rawHeader = 0;
	while(rawHeader == 0) {
		int result = receive_ushort(rawHeader);
		if(result == -1) {
			char debug_msg[128];
			sprintf(debug_msg, "blocking_receive_message: Warning, received header size %i\n.", rawHeader);
			debug_print(debug_msg);
			return -1;
		}
	}
	
	int messageSize = ntohs(rawHeader);
	char debug_msg[128];
	sprintf(debug_msg, "blocking_receive_message: Header with size %i received, waiting for message...\n", messageSize);
	debug_print(debug_msg);
	int result = blocking_receive(messageSize);

	sprintf(debug_msg, "blocking_receive_message: Message received, returning %i.\n", result);
	debug_print(debug_msg);
	return result;
}

int TcpSocket::blocking_receive(int numBytes)
{
	int result = receive_bytes(numBytes, receiveData.packet_buffer);
	
	if(result != -1)
	{
		fill_message_buffer(receiveData.packet_buffer, numBytes);
		char debug_msg[128];
		sprintf(debug_msg, "blocking_receive: Filled godot message buffer with %i bytes.\n", numBytes);
		debug_print(debug_msg);
	} else {
		debug_print("blocking_receive: Error receiving message!\n");
	}

	return result;
}

/*
 * Returns:
 * -1: Socket disconnceted.
 * 0: More data to receive, call again.
 * 1..n: The size of the message received. Godot StreamPeerBuffer contains a full message.
 */
int TcpSocket::non_blocking_receive_message()
{
	if(receiveData.state == NonBlockingState::RECEIVE_HEADER) {
		return non_blocking_receive_header();
	}

	return non_blocking_receive_packet();
}

int TcpSocket::non_blocking_receive_header() {
	int result = receive_bytes(
		receiveData.numBytesToReceive,
		receiveData.header_buffer + receiveData.numBytesReceived);

	if(result > 0) {
		receiveData.numBytesToReceive -= result;
		receiveData.numBytesReceived += result;

		char debug_msg[128];
		sprintf(
			debug_msg,
			"non_blocking_receive_header: Received %i/%i bytes header.\n",
			receiveData.numBytesReceived,
			headerSize);
		debug_print(debug_msg);
	}

	if(receiveData.numBytesReceived == headerSize) {
		if(headerSize == 2) {
			unsigned short tmpHeader = 0;
			memcpy(&tmpHeader, receiveData.header_buffer, 2);
			receiveData.packet_size = ntohs(tmpHeader);
			cout << "non_blocking_receive_header: receiveData.header_buffer" << endl;
			for(int i = 0; i < headerSize; i++) {
				cout << receiveData.header_buffer[i];
			}
			cout << endl;
			cout << "packet_size: " << receiveData.packet_size << endl;
		} else if (headerSize == 4) {
			receiveData.packet_size = ntohl((unsigned int) receiveData.header_buffer);
		}

		memset(receiveData.header_buffer, 0, headerSize);
		receiveData.state = NonBlockingState::RECEIVE_PACKET;
		receiveData.numBytesReceived = 0;
		receiveData.numBytesToReceive = receiveData.packet_size;

		char debug_msg[128];
		sprintf(
			debug_msg,
			"non_blocking_receive_header: Full header received containing messageSize: %i.\n",
			receiveData.packet_size);
		debug_print(debug_msg);
	}
	
	return result;
}

int TcpSocket::non_blocking_receive_packet() {
	int result = receive_bytes(
		receiveData.numBytesToReceive,
		receiveData.packet_buffer + receiveData.numBytesReceived);

	if(result > 0) {
		receiveData.numBytesToReceive -= result;
		receiveData.numBytesReceived += result;
	}

	if(receiveData.numBytesReceived == receiveData.packet_size) {
		fill_message_buffer(receiveData.packet_buffer, receiveData.packet_size);
		
		memset(receiveData.packet_buffer, 0, receiveData.packet_size);
		receiveData.state = NonBlockingState::RECEIVE_HEADER;
		receiveData.packet_size = 0;
		receiveData.numBytesReceived = 0;
		receiveData.numBytesToReceive = headerSize;
	}
	
	return result;
}

int TcpSocket::non_blocking_receive(int numBytes)
{
	int result = receive_bytes(numBytes, receiveData.packet_buffer);
	
	if(result == -1 || result == 0) {
		return result;
	}
	
	fill_message_buffer(receiveData.packet_buffer, numBytes);
	char debug_msg[128];
	sprintf(debug_msg, "blocking_receive: Filled godot message buffer with %i bytes.", numBytes);
	debug_print(debug_msg);

	return result;
}

int TcpSocket::send_message(godot::PackedByteArray sendBuffer)
{
	int numBytes = sendBuffer.size();
	// TODO: Implement header size 4.
	if(headerSize == 2 || headerSize == 4) {
		unsigned short header = numBytes;
		send_ushort(numBytes);
	}

	const char* bytes = (const char*) sendBuffer.ptr();
	_send_bytes(bytes, numBytes);
	return 1;
}

void TcpSocket::fill_message_buffer(char* sourceBuffer, int numBytes)
{
	messageBuffer->seek(0);
	godot::PackedByteArray* poolByteArray = &messageBuffer->get_data_array();
	for (int i = 0; i < numBytes; i++) {
		//poolByteArray->set(i, sourceBuffer[i]);
	}
}

short TcpSocket::ntohs(short var)
{
	return(::ntohs(var));
}

int TcpSocket::htonl(int var)
{
	return(::htonl(var));
}

/*
 * These underscore methods should probably not exist (here), either rename, move them out from this class or merge
 * them into the existing methods.
 */
int TcpSocket::_connect(const char* hostname, int port)
{
	try {
		boost::system::error_code ec;
		std::string portAsString = std::to_string(port);
		ip::tcp::resolver resolver(ioService);
		auto endpoints = resolver.resolve(hostname, portAsString);
	
		if(sslEnabled) {
			boost::asio::connect(secureSocket.next_layer(), endpoints);
			secureSocket.set_verify_mode(ssl::verify_none);
			secureSocket.handshake(ssl::stream_base::client);
			secureSocket.next_layer().non_blocking(!blocking, ec);
		} else {
			boost::asio::connect(tcpSocket, endpoints);

			tcpSocket.non_blocking(!blocking, ec);

			if(ec) {
				cout << "Error setting non-blocking to " << !blocking << " error: " << ec.message() << endl;
			} else {
				cout << "Socket non-blocking set to " << !blocking << endl;
			}
		}
	} catch(boost::system::error_code &error) {
		cout << "Connect failed: " << error.message() << endl;
		lastError = error;
		return 1;
	}

	return 0;
}

void TcpSocket::_close()
{
	if(sslEnabled) {
		secureSocket.shutdown();
	} else {
		tcpSocket.shutdown(ip::tcp::socket::shutdown_both);
	}
}

const int TcpSocket::receive_ushort(unsigned short &header)
{
	int numBytes = sizeof(unsigned short);
	buffer.prepare(numBytes);
	int result = _receive_bytes(numBytes);

	if(result == -1 || result == 0) {
		return result;
	}

	// How to know when full header is received in non-blocking mode?
	header = *buffer_cast<const unsigned short*>(buffer.data());
	buffer.consume(numBytes);
	return result;
}

int TcpSocket::receive_bytes(int numBytes, char* byte_buffer)
{
	buffer.prepare(numBytes);
	int result = _receive_bytes(numBytes);

	if(result == -1 || result == 0) {
		buffer.consume(numBytes);
		return result;
	}

	memcpy(byte_buffer, buffer_cast<const char*>(buffer.data()), result);
	cout << "Received: " << result << " bytes>>>" << endl;
	for(int i = 0; i < result; i++) {
		cout << byte_buffer[i];
	}
	cout << endl;
	buffer.consume(numBytes);
	return result;
}

int TcpSocket::send_ushort(unsigned short ushort)
{
	boost::system::error_code error;
	ushort = htons(ushort);

	if(sslEnabled) {
		boost::asio::write(secureSocket, boost::asio::buffer(&ushort, 2), transfer_all(), error); 
	} else {
		boost::asio::write(tcpSocket, boost::asio::buffer(&ushort, 2), transfer_all(), error);
	}

	cout << "sent ushort: " << error.message() << endl;

	if(error && error != error::eof ) {
		cout << "send failed: " << error.message() << endl;
		lastError = error;
		return -1;
	}

	return 0;
}

int TcpSocket::_send_bytes(const char* bytes, int numBytes)
{
	boost::system::error_code error;
	if(sslEnabled) {
		boost::asio::write(secureSocket, boost::asio::buffer(bytes, numBytes), transfer_all(), error); 
	} else {
		boost::asio::write(tcpSocket, boost::asio::buffer(bytes, numBytes), transfer_all(), error);
	}

	 cout << "sent bytes: " << error.message() << endl;

	if(error && error != error::eof ) {
		cout << "send failed: " << error.message() << endl;
		lastError = error;
		return -1;
	}

	return 0;
}

int TcpSocket::_receive_bytes(int numBytes)
{
	boost::system::error_code error;
	int bytesReceived = 0;

	if(sslEnabled) {
		bytesReceived = read(secureSocket, buffer, transfer_exactly(numBytes), error);
	} else {
		bytesReceived = read(tcpSocket, buffer, transfer_exactly(numBytes), error);
	}

	if(error) {
		if(blocking == false && error == error::would_block) {
			return bytesReceived;
		}

		if(error != error::eof ) {
			cout << "receive failed: " << error.message() << endl;
			lastError = error;
			return -1;
		}
	}

	return bytesReceived;
}

And this is the header file:

/*************************************************************************/
/*  TcpSocket.hpp                                                        */
/*************************************************************************/
/*                       This file is part of:                           */
/*                       GODOT TcpSocket PLUGIN                          */
/*             https://github.com/flodihn/GodotTcpSocket                 */
/*************************************************************************/
/* Copyright (c) 2021 Christian Flodihn.                                 */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#ifndef GODOT_TCP_SOCKET_H
#define GODOT_TCP_SOCKET_H

#include <stdio.h>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/stream_peer_buffer.hpp>
#include <godot_cpp/variant/packed_byte_array.hpp>

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

typedef enum {
	RECEIVE_HEADER,
	RECEIVE_PACKET
} NonBlockingState;

struct ReceiveData {
	NonBlockingState state;
	int numBytesReceived;
	int numBytesToReceive;

	// Internal buffer for receiving the header. The header is allocated
	// to the size of the header set in set_header_size function.
	char* header_buffer;

	// Internal buffer for receiving bytes from the socket,
	// will allocated same number of bytes as the godot stream peer buffer.
	char* packet_buffer;

	int packet_size;
};

namespace godot {

class TcpSocket : public Node {
	GDCLASS(TcpSocket, Node)

private:
	ReceiveData receiveData;
	bool blocking = false;
	bool debug = true;
	bool sslEnabled = false;

	unsigned int headerSize = 0;
	
	boost::asio::streambuf buffer;
	boost::system::error_code lastError;

	// External buffer used to send back bytes to Godot when receiving full
	// message with 2 or 4 byte header.
	StreamPeerBuffer* messageBuffer;

	int blocking_receive(int numBytes);
	int non_blocking_receive(int numBytes);

	void debug_print(const char * output);
	void fill_message_buffer(char*, int messageSize);

	int blocking_receive_message();
	int non_blocking_receive_message();

	int non_blocking_receive_header();
	int non_blocking_receive_packet();

	int _connect(const char* hostname, int port);
	void _close();
	const int receive_ushort(unsigned short &header);
	int receive_bytes(int numBytes, char* byte_buffer);
	int send_ushort(unsigned short ushort);
	int _send_bytes(const char* bytes, int numBytes);
	int _receive_bytes(int numBytes);

protected:
	static void _bind_methods();

public:
	TcpSocket();
	~TcpSocket();

	void _process(double delta);

	int connect_to_host(Variant host, int port);
	void close();

	void set_message_header_size(int size);
	void set_message_buffer(Ref<godot::StreamPeerBuffer> messageBufferRef);
	void set_ssl(bool trueOrFalse);
	void set_blocking(bool trueOrFalse);
	void set_debug(bool trueOrFalse);
	void set_receive_buffer(unsigned size);

	int receive_message();
	int send_message(PackedByteArray sendBuffer);

	short ntohs(short var);
	int htonl(int var);
};

}

#endif
Misdeed answered 1/8, 2023 at 10:5 Comment(0)
P
0

I'm wondering if the parser is getting confused by _closebeing declared as a privatefunction. Obviously, this shouldn't an issue.

If you just want it to work, you could change the function from TcpSocket::close() to TcpSocket::gd_close() (along with the declaration in the header file) and then bind it under the new name:

ClassDB::bind_method(D_METHOD("close"), &TcpSocket::gd_close);

Edit: On re-reading this, I see the code is compiling, but not executing. Still, trying this might provide some clues as to what the issue is.

Paver answered 2/8, 2023 at 19:44 Comment(0)
M
0

I found a clue, when out comment the boost includes everything seem to work. So the problem is boost again, I had some issues with it for Godot 3.5 but I managed to find an ugly workaround for it.

I think the solution to my problem is to use something else than boost.

Misdeed answered 20/8, 2023 at 16:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.